.LÍQUIDO:Como invoco um delegado em um thread específico?(ISynchronizeInvoke, Dispatcher, AsyncOperation, SynchronizationContext, etc.)

StackOverflow https://stackoverflow.com/questions/4843010

Pergunta

Observe antes de tudo que esta pergunta não está marcada ou ou qualquer outra coisa específica da GUI.Isso é intencional, como você verá em breve.

Em segundo lugar, desculpe se esta pergunta é um pouco longa.Tento reunir vários pedaços de informação que flutuam aqui e ali para também fornecer informações valiosas.Minha pergunta, porém, está em "O que eu gostaria de saber".

Minha missão é finalmente entender as várias maneiras oferecidas pelo .NET para invocar um delegado em um thread específico.


O que eu gostaria de saber:

  • Estou procurando a maneira mais geral possível (que não seja específica do Winforms ou do WPF) para invocar delegados em threads específicos.

  • Ou, dito de forma diferente:Eu estaria interessado se e como as várias maneiras de fazer isso (como por meio do WPF Dispatcher) fazer uso um do outro;isto é, se houver um mecanismo comum para invocação de delegado entre threads que seja usado por todos os outros.


O que eu já sei:

  • Existem muitas aulas relacionadas a este tema;entre eles:

    • SynchronizationContext (em System.Threading)
      Se eu tivesse que adivinhar, esse seria o mais básico;embora eu não entenda exatamente o que faz, nem como é usado.

    • AsyncOperation & AsyncOperationManager (em System.ComponentModel)
      Estes parecem ser invólucros ao redor SynchronizationContext.Não tenho ideia de como usá-los.

    • WindowsFormsSynchronizationContext (em System.Windows.Forms)
      Uma subclasse de SynchronizationContext.

    • ISynchronizeInvoke (em System.ComponentModel)
      Usado pelo Windows Forms.(O Control classe implementa isso.Se eu tivesse que adivinhar, diria que esta implementação faz uso de WindowsFormsSynchronizationContext.)

    • Dispatcher &DispatcherSynchronizationContext (em System.Windows.Threading)
      Parece que o último é outra subclasse de SynchronizationContext, e os ex-delegados a ele.

  • Alguns threads possuem seu próprio loop de mensagens, juntamente com uma fila de mensagens.

    (A página do MSDN Sobre mensagens e filas de mensagens tem algumas informações introdutórias sobre como os loops de mensagens funcionam no nível do sistema, ou seja,filas de mensagens como a API do Windows.)

    Posso ver como alguém implementaria a invocação entre threads para threads com uma fila de mensagens.Usando a API do Windows, você colocaria uma mensagem na fila de mensagens de um thread específico via PostThreadMessage que contém uma instrução para chamar algum delegado.O loop de mensagem — que é executado nesse thread — eventualmente chegará a essa mensagem e o delegado será chamado.

    Pelo que li no MSDN, um thread não terá automaticamente sua própria fila de mensagens.Uma fila de mensagens ficará disponível, por exemplo.quando um thread criou uma janela.Sem uma fila de mensagens, não faz sentido que um thread tenha um loop de mensagens.

    Então, a invocação de delegado entre threads é possível quando o thread de destino não possui loop de mensagem?Digamos, em um aplicativo de console .NET?(A julgar pelas respostas a essa questão, suponho que seja realmente impossível com aplicativos de console.)

Foi útil?

Solução

Desculpe por postar uma resposta tão longa.Mas achei que valia a pena explicar o que exatamente está acontecendo.

A-há!Acho que já descobri.A maneira mais genérica de invocar um delegado em um thread específico parece ser a SynchronizationContext aula.

Primeiro, o framework .NET faz não fornecer um meio padrão para simplesmente "enviar" um delegado para qualquer thread de forma que ele seja executado imediatamente.Obviamente, isso não pode funcionar, porque significaria “interromper” qualquer trabalho que aquele thread estivesse fazendo no momento.Portanto, o próprio thread de destino decide como e quando "receberá" delegados;isto é, esta funcionalidade deve ser fornecida pelo programador.

Portanto, um thread de destino precisa de alguma forma de "receber" delegados.Isso pode ser feito de muitas maneiras diferentes.Um mecanismo fácil é o thread sempre retornar a algum loop (vamos chamá-lo de "loop de mensagem") onde examinará uma fila.Funcionará com tudo o que estiver na fila.O Windows funciona nativamente assim quando se trata de coisas relacionadas à interface do usuário.

A seguir, demonstrarei como implementar uma fila de mensagens e um SynchronizationContext para isso, bem como um tópico com um loop de mensagens.Por fim, demonstrarei como invocar um delegado nesse thread.


Exemplo:

Passo 1. Vamos primeiro Crie um SynchronizationContext aula que será usado junto com a fila de mensagens do thread de destino:

class QueueSyncContext : SynchronizationContext
{
    private readonly ConcurrentQueue<SendOrPostCallback> queue;

    public QueueSyncContext(ConcurrentQueue<SendOrPostCallback> queue)
    {
        this.queue = queue;
    }

    public override void Post(SendOrPostCallback d, object state)
    {
        queue.Enqueue(d);
    }

    // implementation for Send() omitted in this example for simplicity's sake.
}

Basicamente, isso não faz mais do que adicionar todos os delegados que são passados ​​via Post para uma fila fornecida pelo usuário.(Post é o método para invocações assíncronas. Send seria para invocações síncronas.Estou omitindo o último por enquanto.)

Passo 2. Vamos agora escrever o código para um tópico Z que espera por delegados d chegar:

SynchronizationContext syncContextForThreadZ = null;

void MainMethodOfThreadZ()
{
    // this will be used as the thread's message queue:
    var queue = new ConcurrentQueue<PostOrCallDelegate>();

    // set up a synchronization context for our message processing:
    syncContextForThreadZ = new QueueSyncContext(queue);
    SynchronizationContext.SetSynchronizationContext(syncContextForThreadZ);

    // here's the message loop (not efficient, this is for demo purposes only:)
    while (true)
    {
        PostOrCallDelegate d = null;
        if (queue.TryDequeue(out d))
        {
            d.Invoke(null);
        }
    }
}

Etapa 3. Fio Z precisa ser iniciado em algum lugar:

new Thread(new ThreadStart(MainMethodOfThreadZ)).Start();

Passo 4. Finalmente, de volta em algum outro tópico A, queremos enviar um delegado para o thread Z:

void SomeMethodOnThreadA()
{
    // thread Z must be up and running before we can send delegates to it:
    while (syncContextForThreadZ == null) ;

    syncContextForThreadZ.Post(_ =>
        {
            Console.WriteLine("This will run on thread Z!");
        },
        null);
}

O bom disso é que SynchronizationContext funciona, não importa se você está em um aplicativo Windows Forms, em um aplicativo WPF ou em um aplicativo de console multithread de sua própria criação.Tanto o Winforms quanto o WPF fornecem e instalam SynchronizationContexts para seu thread principal/UI.

O procedimento geral para invocar um delegado em um thread específico é o seguinte:

  • Você deve capturar o thread de destino (Zde) SynchronizationContext, de forma que você possa Send (sincronicamente) ou Post (de forma assíncrona) um delegado para esse thread.A maneira de fazer isso é armazenar o contexto de sincronização retornado por SynchronizationContext.Current enquanto você está no tópico de destino Z.(Este contexto de sincronização deve ter sido previamente registrado em/por thread Z.) Em seguida, armazene essa referência em algum lugar onde seja acessível por thread A.

  • Enquanto estiver no tópico A, você pode usar o contexto de sincronização capturado para enviar ou postar qualquer delegado no thread Z: zSyncContext.Post(_ => { ... }, null);

Outras dicas

Se você deseja oferecer suporte à chamada de um delegado em um thread que, de outra forma, não possui um loop de mensagens, você deve implementar o seu próprio, basicamente.

Não há nada particularmente mágico em um loop de mensagens:é como um consumidor num padrão normal de produtor/consumidor.Ele mantém uma fila de coisas para fazer (normalmente eventos aos quais reagir) e passa pela fila agindo de acordo.Quando não há mais nada a fazer, ele espera até que algo seja colocado na fila.

Dito de outra forma:você pode pensar em um thread com um loop de mensagem como um pool de threads de thread único.

Você pode implementar isso com bastante facilidade, inclusive em um aplicativo de console.Apenas lembre-se de que se o thread estiver circulando pela fila de trabalho, ele não poderá estar fazendo outra coisa também - enquanto normalmente o thread principal de execução em um aplicativo de console se destina a executar uma sequência de tarefas e depois terminar.

Se você estiver usando o .NET 4, é muito fácil implementar uma fila de produtor/consumidor usando o BlockingCollection aula.

Recentemente me deparei com este artigo e descobri que ele é um salva-vidas.O uso de uma fila simultânea de bloqueio é o ingrediente secreto, como apontado por Jon Skeet acima.O melhor 'como fazer' que encontrei para fazer todo esse trabalho é Este artigo no CodeProject por Mike Peretz.O artigo faz parte de uma série de três partes sobre SynchronizationContext que fornece exemplos de código que podem ser facilmente transformados em código de produção.Observe que Peretz apenas preenche todos os detalhes, mas também nos lembra que o SynchronizationContext base tem implementações essencialmente inúteis de Post() e Send() e, portanto, realmente deve ser visto como uma classe base abstrata.Um usuário casual da classe base ficaria surpreso ao descobrir que ela não resolve os problemas do mundo real.

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