O que o SynchronizationContext faz?

135

No livro Programming C #, há alguns exemplos de código sobre SynchronizationContext:

SynchronizationContext originalContext = SynchronizationContext.Current;
ThreadPool.QueueUserWorkItem(delegate {
    string text = File.ReadAllText(@"c:\temp\log.txt");
    originalContext.Post(delegate {
        myTextBox.Text = text;
    }, null);
});

Como sou iniciante em tópicos, responda em detalhes. Primeiro, não sei o que significa contexto, o que o programa salva no originalContext? E quando o Postmétodo é acionado, o que o thread da interface do usuário fará?
Se eu perguntar algumas coisas tolas, por favor, corrija-me, obrigado!

EDIT: Por exemplo, e se eu apenas escrever myTextBox.Text = text;no método, qual é a diferença?

cloudyFan
fonte
1
O manual fino tem a dizer O objetivo do modelo de sincronização implementado por esta classe é permitir que as operações assíncronas / de sincronização internas do Common Language Runtime se comportem adequadamente com diferentes modelos de sincronização. Esse modelo também simplifica alguns dos requisitos que os aplicativos gerenciados tiveram que seguir para funcionar corretamente em diferentes ambientes de sincronização.
ta.speot.is
IMHO assíncrona aguardam já faz isso
Royi Namir
7
@RoyiNamir: Sim, mas adivinhem: async/ awaitconfia em SynchronizationContextbaixo.
stakx - não está mais contribuindo em

Respostas:

170

O que o SynchronizationContext faz?

Simplificando, SynchronizationContextrepresenta um local "onde" o código pode ser executado. Os delegados que são passados ​​para o métodoSend ou serão então chamados nesse local. ( é a versão sem bloqueio / assíncrona de .)PostPostSend

Cada encadeamento pode ter uma SynchronizationContextinstância associada a ele. O fio condutor pode ser associada a um contexto de sincronização chamando o estático SynchronizationContext.SetSynchronizationContextmétodo eo contexto atual do segmento em execução pode ser consultado através da SynchronizationContext.Currentpropriedade .

Apesar do que acabei de escrever (cada segmento com um contexto de sincronização associado), a SynchronizationContextnão representa necessariamente um segmento específico ; ele também pode encaminhar a chamada dos delegados transmitidos a ele para qualquer um dos vários threads (por exemplo, para um ThreadPoolthread de trabalho) ou (pelo menos em teoria) para um núcleo específico da CPU ou mesmo para outro host de rede . O local em que seus delegados acabam executando depende do tipo de SynchronizationContextuso.

O Windows Forms instalará um WindowsFormsSynchronizationContextno thread no qual o primeiro formulário é criado. (Esse thread geralmente é chamado de "o thread da interface do usuário".) Esse tipo de contexto de sincronização chama os delegados transmitidos a ele exatamente nesse thread. Isso é muito útil, pois o Windows Forms, como muitas outras estruturas de interface do usuário, permite apenas a manipulação de controles no mesmo encadeamento no qual eles foram criados.

E se eu apenas escrever myTextBox.Text = text;no método, qual é a diferença?

O código para o qual você passou ThreadPool.QueueUserWorkItemserá executado em um thread de trabalho do conjunto de threads. Ou seja, ele não será executado no thread em que você myTextBoxfoi criado, portanto, o Windows Forms, mais cedo ou mais tarde (especialmente nas versões do Release) lançará uma exceção, informando que você não pode acessar myTextBoxatravés de outro thread.

É por isso que você precisa, de alguma forma, "voltar" do segmento de trabalho para o "segmento de interface do usuário" (onde myTextBoxfoi criado) antes dessa atribuição específica. Isto se faz do seguinte modo:

  1. Enquanto você ainda estiver no thread da interface do usuário, capture o Windows Forms ' SynchronizationContextlá e armazene uma referência a ele em uma variável ( originalContext) para uso posterior. Você deve consultar SynchronizationContext.Currentneste momento; se você o consultou dentro do código passado ThreadPool.QueueUserWorkItem, poderá obter qualquer contexto de sincronização associado ao segmento de trabalho do conjunto de encadeamentos. Depois de armazenar uma referência ao contexto do Windows Forms, você pode usá-la em qualquer lugar e a qualquer momento para "enviar" o código ao thread da interface do usuário.

  2. Sempre que você precisar manipular um elemento da interface do usuário (mas não estiver mais ou não estiver no encadeamento da interface do usuário), acesse o contexto de sincronização do Windows Forms por meio de originalContexte entregue o código que manipulará a interface do usuário para um Sendou outro Post.


Considerações finais e dicas:

  • O que os contextos de sincronização não farão por você é dizer qual código deve ser executado em um local / contexto específico e qual código pode ser executado normalmente normalmente, sem passar para a SynchronizationContext. Para decidir isso, você deve conhecer as regras e os requisitos da estrutura em que está programando - Windows Forms nesse caso.

    Portanto, lembre-se desta regra simples para Windows Forms: NÃO acesse controles ou formulários de um thread diferente daquele que os criou. Se você deve fazer isso, use o SynchronizationContextmecanismo conforme descrito acima ou Control.BeginInvoke(que é uma maneira específica de fazer o Windows Forms de fazer exatamente a mesma coisa).

  • Se você está programando contra .NET 4.5 ou posterior, você pode tornar sua vida muito mais fácil através da conversão de seu código que explicitamente usos SynchronizationContext, ThreadPool.QueueUserWorkItem, control.BeginInvoke, etc. para as novas async/ awaitpalavras-chave e a biblioteca paralela de tarefas (TPL) , ou seja, a API circundante as classes Taske Task<TResult>. Eles, em um nível muito alto, cuidarão da captura do contexto de sincronização do encadeamento da interface do usuário, iniciando uma operação assíncrona e voltando ao encadeamento da interface do usuário para que você possa processar o resultado da operação.

stakx - não está mais contribuindo
fonte
Você diz que o Windows Forms, como muitas outras estruturas de interface do usuário, permite apenas a manipulação de controles no mesmo thread, mas todas as janelas no Windows devem ser acessadas pelo mesmo thread que o criou.
usar o seguinte comando
4
@ user34660: Não, isso não está correto. Você pode ter vários segmentos que criam controles do Windows Forms. Mas cada controle está associado ao único segmento que o criou e deve ser acessado apenas por esse único segmento. Os controles de diferentes threads da interface do usuário também são muito limitados na maneira como eles interagem: um não pode ser o pai / filho do outro, a ligação de dados entre eles não é possível etc. Por fim, cada thread que cria controles precisa de sua própria mensagem loop (que é iniciado pelo Application.RunIIRC). Este é um tópico bastante avançado e não algo feito casualmente.
stakx - não contribui mais com
Meu primeiro comentário se deve ao fato de você dizer "como muitas outras estruturas de interface do usuário", o que implica que algumas janelas permitem "manipulação de controles" de um thread diferente, mas nenhuma janela do Windows. Você não pode "ter vários threads que criam controles do Windows Forms" para a mesma janela e "deve ser acessado pelo mesmo thread" e "deve ser acessado apenas pelo mesmo thread" dizendo a mesma coisa. Duvido que seja possível criar "Controles de diferentes segmentos da interface do usuário" para a mesma janela. Tudo isso não é avançado para aqueles com experiência em programação do Windows antes do .Net.
user34660
3
Toda essa conversa sobre "janelas" e "janelas Windows" está me deixando um pouco tonta. Eu mencionei alguma dessas "janelas"? Eu acho que não ...
stakx - não contribui mais com
1
@ibubi: Não sei se entendi sua pergunta. O contexto de sincronização de qualquer encadeamento não está definido ( null) ou é uma instância SynchronizationContext(ou uma subclasse dele). O objetivo dessa citação não era o que você obtinha, mas o que não obtinha: o contexto de sincronização do thread da interface do usuário.
stakx - não está mais contribuindo com o
24

Eu gostaria de adicionar a outras respostas, SynchronizationContext.Postapenas enfileirar um retorno de chamada para execução posterior no segmento de destino (normalmente durante o próximo ciclo do loop de mensagens do segmento de destino) e, em seguida, a execução continuará no segmento de chamada. Por outro lado, SynchronizationContext.Sendtenta executar o retorno de chamada no thread de destino imediatamente, o que bloqueia o thread de chamada e pode resultar em conflito. Nos dois casos, existe a possibilidade de reentrada de código (inserir um método de classe no mesmo encadeamento de execução antes que a chamada anterior ao mesmo método retorne).

Se você estiver familiarizado com o modelo de programação Win32, seria uma analogia muito próxima PostMessagee as SendMessageAPIs, que você pode chamar para enviar uma mensagem de um thread diferente daquele da janela de destino.

Aqui está uma explicação muito boa de quais são os contextos de sincronização: É tudo sobre o SynchronizationContext .

noseratio
fonte
16

Ele armazena o provedor de sincronização, uma classe derivada de SynchronizationContext. Nesse caso, provavelmente será uma instância do WindowsFormsSynchronizationContext. Essa classe usa os métodos Control.Invoke () e Control.BeginInvoke () para implementar os métodos Send () e Post (). Ou pode ser DispatcherSynchronizationContext, usa Dispatcher.Invoke () e BeginInvoke (). Em um aplicativo WinForms ou WPF, esse provedor é instalado automaticamente assim que você cria uma janela.

Quando você executa o código em outro encadeamento, como o encadeamento de conjunto de encadeamentos usado no snippet, é necessário ter cuidado para não usar diretamente objetos que não são seguros. Como qualquer objeto de interface do usuário, você deve atualizar a propriedade TextBox.Text do thread que criou o TextBox. O método Post () garante que o destino delegado seja executado nesse segmento.

Tenha em atenção que este fragmento é um pouco perigoso, só funcionará corretamente quando você o chamar do thread da interface do usuário. SynchronizationContext.Current possui valores diferentes em diferentes segmentos. Somente o thread da interface do usuário tem um valor utilizável. E é a razão pela qual o código teve que copiá-lo. Uma maneira mais legível e segura de fazer isso, em um aplicativo Winforms:

    ThreadPool.QueueUserWorkItem(delegate {
        string text = File.ReadAllText(@"c:\temp\log.txt");
        myTextBox.BeginInvoke(new Action(() => {
            myTextBox.Text = text;
        }));
    });

O que tem a vantagem de funcionar quando chamado de qualquer thread. A vantagem de usar SynchronizationContext.Current é que ainda funciona se o código é usado no Winforms ou no WPF, é importante em uma biblioteca. Esse certamente não é um bom exemplo desse código; você sempre sabe que tipo de TextBox você tem aqui; portanto, sempre sabe se deve usar Control.BeginInvoke ou Dispatcher.BeginInvoke. Na verdade, usar SynchronizationContext.Current não é tão comum.

O livro está tentando ensiná-lo sobre a segmentação, portanto, usar este exemplo defeituoso é aceitável. Na vida real, nos poucos casos em que você pode considerar usar SynchronizationContext.Current, você ainda pode deixar as palavras-chave assíncronas / em espera do C # ou TaskScheduler.FromCurrentSynchronizationContext () para fazer isso por você. Mas observe que eles ainda se comportam mal da maneira que o trecho faz quando você os usa no encadeamento errado, pelo mesmo motivo. Uma pergunta muito comum por aqui, o nível extra de abstração é útil, mas torna mais difícil descobrir por que eles não funcionam corretamente. Espero que o livro também diga quando não usá-lo :)

Hans Passant
fonte
Sinto muito, por que deixar o thread de interface do usuário manipular é seguro para thread? ou seja, acho que o thread da interface do usuário poderia estar usando myTextBox quando o Post () disparou, isso é seguro?
usar o seguinte código
4
É difícil decodificar seu inglês. Seu snippet original funciona apenas corretamente quando é chamado a partir do thread da interface do usuário. Qual é um caso muito comum. Somente então ele será postado de volta no thread da interface do usuário. Se for chamado de um encadeamento de trabalho, o destino do delegado Post () será executado em um encadeamento de conjunto de encadeamentos. Kaboom. Isso é algo que você deseja tentar por si mesmo. Inicie um thread e deixe o thread chamar esse código. Você fez isso corretamente se o código falhar com uma NullReferenceException.
Hans Passant
5

O objetivo do contexto de sincronização aqui é garantir que isso myTextbox.Text = text;seja chamado no thread principal da interface do usuário.

O Windows exige que os controles da GUI sejam acessados ​​apenas pelo thread com o qual foram criados. Se você tentar atribuir o texto em um encadeamento em segundo plano sem primeiro sincronizar (por qualquer um dos vários meios, como este ou o padrão Invoke), uma exceção será lançada.

O que isso faz é salvar o contexto de sincronização antes de criar o encadeamento em segundo plano e, em seguida, o encadeamento em segundo plano usa o método context.Post, execute o código da GUI.

Sim, o código que você mostrou é basicamente inútil. Por que criar um thread em segundo plano, apenas para voltar imediatamente ao thread principal da interface do usuário? É apenas um exemplo.

Erik Funkenbusch
fonte
4
"Sim, o código que você mostrou é basicamente inútil. Por que criar um thread em segundo plano, apenas para voltar imediatamente ao thread principal da interface do usuário? É apenas um exemplo." - Leitura de um arquivo pode ser uma tarefa longa se o arquivo é grande, algo que pode bloquear o thread UI e torná-lo sem resposta
Yair Nevet
Eu tenho uma pergunta estúpida. Cada thread tem um ID e suponho que o thread da interface do usuário também tenha um ID = 2, por exemplo. Então, quando estou no thread do pool de threads, posso fazer algo assim: var thread = GetThread (2); thread.Execute (() => textbox1.Text = "foo")?
John John
@ John - Não, não acho que funcione porque o thread já está em execução. Você não pode executar um encadeamento já em execução. Executar funciona apenas quando um encadeamento não está sendo executado (IIRC)
Erik Funkenbusch
3

Para a fonte

Cada encadeamento possui um contexto associado a ele - também conhecido como contexto "atual" - e esses contextos podem ser compartilhados entre encadeamentos. O ExecutionContext contém metadados relevantes do ambiente ou contexto atual em que o programa está em execução. O SynchronizationContext representa uma abstração - indica o local onde o código do seu aplicativo é executado.

Um SynchronizationContext permite enfileirar uma tarefa em outro contexto. Observe que cada encadeamento pode ter seu próprio SynchronizatonContext.

Por exemplo: Suponha que você tenha dois threads, Thread1 e Thread2. Digamos, o Thread1 está fazendo algum trabalho e o Thread1 deseja executar o código no Thread2. Uma maneira possível de fazer isso é solicitar ao objeto SynchronizationContext do Thread2, fornecê-lo ao Thread1 e, em seguida, o Thread1 pode chamar SynchronizationContext.Send para executar o código no Thread2.

Olhos grandes
fonte
2
Um contexto de sincronização não está necessariamente vinculado a um segmento específico. É possível que vários threads processem solicitações em um único contexto de sincronização e que um único thread processe solicitações para vários contextos de sincronização.
Servy
3

SynchronizationContext fornece uma maneira de atualizar uma interface do usuário de um thread diferente (de forma síncrona pelo método Send ou de forma assíncrona pelo método Post).

Veja o seguinte exemplo:

    private void SynchronizationContext SyncContext = SynchronizationContext.Current;
    private void Button_Click(object sender, RoutedEventArgs e)
    {
        Thread thread = new Thread(Work1);
        thread.Start(SyncContext);
    }

    private void Work1(object state)
    {
        SynchronizationContext syncContext = state as SynchronizationContext;
        syncContext.Post(UpdateTextBox, syncContext);
    }

    private void UpdateTextBox(object state)
    {
        Thread.Sleep(1000);
        string text = File.ReadAllText(@"c:\temp\log.txt");
        myTextBox.Text = text;
    }

SynchronizationContext.Current retornará o contexto de sincronização do thread da interface do usuário. Como eu sei disso? No início de todos os formulários ou aplicativos WPF, o contexto será definido no thread da interface do usuário. Se você criar um aplicativo WPF e executar o meu exemplo, verá que, quando clica no botão, ele dorme por aproximadamente 1 segundo e mostra o conteúdo do arquivo. Você pode esperar que não, porque o chamador do método UpdateTextBox (que é Work1) é um método passado para um Thread; portanto, ele deve adormecer esse thread, não o thread principal da interface do usuário, NOPE! Mesmo que o método Work1 seja passado para um thread, observe que ele também aceita um objeto que é o SyncContext. Se você observar, verá que o método UpdateTextBox é executado pelo método syncContext.Post e não pelo método Work1. Veja o seguinte:

private void Button_Click(object sender, RoutedEventArgs e) 
{
    Thread.Sleep(1000);
    string text = File.ReadAllText(@"c:\temp\log.txt");
    myTextBox.Text = text;
}

O último exemplo e este executam o mesmo. Ambos não bloqueiam a interface do usuário enquanto o fazem.

Em conclusão, pense em SynchronizationContext como um encadeamento. Não é um segmento, ele define um segmento (observe que nem todos os segmentos têm um SyncContext). Sempre que chamamos o método Post ou Send para atualizar uma interface do usuário, é como atualizar a interface do usuário normalmente a partir do thread principal da interface do usuário. Se, por algum motivo, você precisar atualizar a interface do usuário a partir de um thread diferente, verifique se o thread possui o SyncContext do thread principal da interface do usuário e chame o método Send ou Post com o método que você deseja executar e pronto. conjunto.

Espero que isso ajude você, companheiro!

Marc2001
fonte
2

O SynchronizationContext é basicamente um provedor de execução de delegados de retorno de chamada, responsável principalmente por garantir que os delegados sejam executados em um determinado contexto de execução após uma parte específica do código (incluída no Objeto de tarefa do .Net TPL) de um programa concluir sua execução.

Do ponto de vista técnico, o SC é uma classe C # simples, orientada para oferecer suporte e fornecer sua função especificamente para objetos da Biblioteca Paralela de Tarefas.

Todo aplicativo .Net, exceto os aplicativos de console, possui uma implementação específica dessa classe com base na estrutura subjacente específica, ou seja: WPF, WindowsForm, Asp Net, Silverlight, etc.

A importância desse objeto está vinculada à sincronização entre resultados retornados da execução assíncrona do código e a execução do código dependente que está aguardando resultados desse trabalho assíncrono.

E a palavra "contexto" significa contexto de execução, que é o contexto de execução atual em que o código em espera será executado, ou seja, a sincronização entre o código assíncrono e o código em espera ocorre em um contexto de execução específico, portanto, esse objeto é chamado SynchronizationContext: representa o contexto de execução que cuidará da sincronização do código assíncrono e da execução do código em espera .

Ciro Corvino
fonte
1

Este exemplo é dos exemplos de Joseph Albahari no Linqpad, mas realmente ajuda a entender o que o contexto de Sincronização faz.

void WaitForTwoSecondsAsync (Action continuation)
{
    continuation.Dump();
    var syncContext = AsyncOperationManager.SynchronizationContext;
    new Timer (_ => syncContext.Post (o => continuation(), _)).Change (2000, -1);
}

void Main()
{
    Util.CreateSynchronizationContext();
    ("Waiting on thread " + Thread.CurrentThread.ManagedThreadId).Dump();
    for (int i = 0; i < 10; i++)
        WaitForTwoSecondsAsync (() => ("Done on thread " + Thread.CurrentThread.ManagedThreadId).Dump());
}
loneshark99
fonte