Pergunta

Sinto que deveria saber a resposta para isso, mas vou perguntar de qualquer maneira, caso esteja cometendo um erro potencialmente catastrófico.

O código a seguir é executado conforme esperado, sem erros/exceções:

static void Main(string[] args)
{
    ManualResetEvent flag = new ManualResetEvent(false);
    ThreadPool.QueueUserWorkItem(s =>
    {
        flag.WaitOne();
        Console.WriteLine("Work Item 1 Executed");
    });
    ThreadPool.QueueUserWorkItem(s =>
    {
        flag.WaitOne();
        Console.WriteLine("Work Item 2 Executed");
    });
    Thread.Sleep(1000);
    flag.Set();
    flag.Close();
    Console.WriteLine("Finished");
}

É claro que, como geralmente acontece com código multithread, um teste bem-sucedido não prova que ele seja realmente seguro para threads.O teste também será bem-sucedido se eu colocar Close antes Set, embora a documentação afirme claramente que tentar fazer qualquer coisa depois de um Close resultará em comportamento indefinido.

A minha pergunta é: quando invoco o ManualResetEvent.Set método, é garantido sinalizar todos aguardando tópicos antes devolvendo o controle ao chamador?Por outras palavras, assumindo que posso garantir que não haverá mais chamadas para WaitOne, é seguro fechar o identificador aqui ou é possível que, em algumas circunstâncias, esse código impeça que alguns garçons sejam sinalizados ou resulte em um ObjectDisposedException?

A documentação diz apenas que Set coloca-o em um "estado sinalizado" - não parece fazer nenhuma afirmação sobre quando os garçons irão realmente conseguir esse sinal, então eu gostaria de ter certeza.

Foi útil?

Solução

Quando você sinaliza com um ManualResetEvent.Set Você tem garantia de que todos os tópicos que estão esperando por esse evento (ou seja, em um estado de bloqueio em flag.WaitOne) será sinalizado antes de retornar o controle ao chamador.

É claro que há circunstâncias em que você pode definir a bandeira e seu tópico não o vê porque está fazendo algum trabalho antes de verificar a bandeira (ou como os Nobugs sugeriram se você estiver criando vários threads):

ThreadPool.QueueUserWorkItem(s =>
{
    QueryDB();
    flag.WaitOne();
    Console.WriteLine("Work Item 1 Executed");
});

Há disputa na bandeira e agora você pode criar um comportamento indefinido quando o fechar. Sua bandeira é um recurso compartilhado entre seus threads, você deve criar uma trava de contagem regressiva na qual cada thread sinaliza após a conclusão. Isso eliminará a contenção em seu flag.

public class CountdownLatch
{
    private int m_remain;
    private EventWaitHandle m_event;

    public CountdownLatch(int count)
    {
        Reset(count);
    }

    public void Reset(int count)
    {
        if (count < 0)
            throw new ArgumentOutOfRangeException();
        m_remain = count;
        m_event = new ManualResetEvent(false);
        if (m_remain == 0)
        {
            m_event.Set();
        }
    }

    public void Signal()
    {
        // The last thread to signal also sets the event.
        if (Interlocked.Decrement(ref m_remain) == 0)
            m_event.Set();
    }

    public void Wait()
    {
        m_event.WaitOne();
    }
}
  1. Cada encadeamento sinais na trava da contagem regressiva.
  2. Seu tópico principal aguarda a trava da contagem regressiva.
  3. O fio principal limpa após os sinais de trava da contagem regressiva.

Por fim, o tempo que você dorme no final não é uma maneira segura de cuidar de seus problemas; em vez disso, você deve projetar seu programa para que seja 100% seguro em um ambiente multithread.

Atualização: produtor único/múltiplos consumidores
A presunção aqui é que seu produtor sabe quantos consumidores serão criados, Depois de criar todos os seus consumidores, você redefine o CountdownLatch com o número dado de consumidores:

// In the Producer
ManualResetEvent flag = new ManualResetEvent(false);
CountdownLatch countdown = new CountdownLatch(0);
int numConsumers = 0;
while(hasMoreWork)
{
    Consumer consumer = new Consumer(coutndown, flag);
    // Create a new thread with each consumer
    numConsumers++;
}
countdown.Reset(numConsumers);
flag.Set();
countdown.Wait();// your producer waits for all of the consumers to finish
flag.Close();// cleanup

Outras dicas

Não está bem. Você está tendo sorte aqui porque apenas começou dois tópicos. Eles começarão imediatamente a correr quando você ligar para o conjunto de um núcleo duplo. Experimente isso e assista a bomba:

    static void Main(string[] args) {
        ManualResetEvent flag = new ManualResetEvent(false);
        for (int ix = 0; ix < 10; ++ix) {
            ThreadPool.QueueUserWorkItem(s => {
                flag.WaitOne();
                Console.WriteLine("Work Item Executed");
            });
        }
        Thread.Sleep(1000);
        flag.Set();
        flag.Close();
        Console.WriteLine("Finished");
        Console.ReadLine();
    }

Seu código original falhará da mesma forma em uma máquina antiga ou em sua máquina atual quando estiver muito ocupada com outras tarefas.

Minha opinião é que existe uma condição de corrida.Ao escrever objetos de evento com base em variáveis ​​de condição, você obtém um código como este:

mutex.lock();
while (!signalled)
    condition_variable.wait(mutex);
mutex.unlock();

Portanto, embora o evento possa ser sinalizado, o código que aguarda o evento ainda pode precisar de acesso a partes do evento.

De acordo com a documentação em Fechar, isso libera apenas recursos não gerenciados.Portanto, se o evento usar apenas recursos gerenciados, você poderá ter sorte.Mas isso pode mudar no futuro, então eu erraria por precaução e não encerraria o evento até que você saiba que ele não está mais sendo usado.

Parece um padrão arriscado para mim, mesmo que, devido à implementação [atual], está tudo bem. Você está tentando descartar um recurso que ainda pode estar em uso.

É como recompensar e construir um objeto e excluí -lo cegamente, mesmo antes que os consumidores desse objeto sejam feitos.

Mesmo caso contrário, existe um problema aqui. O programa pode sair, mesmo antes de outros tópicos terem a chance de correr. Os threads do pool de threads são threads de fundo.

Dado que você precisa esperar por outros tópicos de qualquer maneira, também pode limpar depois.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top