Na minha prática, geralmente encontro, em um ambiente diferente , código como o abaixo:
[1] var x = FooWithResultAsync(/*...*/).Result; // [2] FooAsync(/*...*/).Wait(); // [3] FooAsync(/*...*/).GetAwaiter().GetResult(); // [4] FooAsync(/*...*/) .ConfigureAwait(false) .GetAwaiter() .GetResult(); // [5] await FooAsync(/*...*/).ConfigureAwait(false) // [6] await FooAsync(/*...*/)
Com a comunicação com os autores dessas linhas, ficou claro que todas elas são divididas em três grupos:
- O primeiro grupo é aquele que não sabe nada sobre possíveis problemas com a chamada
Result/Wait/GetResult
. Os exemplos (1-3) e às vezes (6) são típicos para programadores deste grupo; - O segundo grupo inclui programadores que estão cientes de possíveis problemas, mas eles não sabem as causas de sua ocorrência. Os desenvolvedores deste grupo, por um lado, tentam evitar linhas como (1-3 e 6), mas, por outro lado, abusam de códigos como (4-5);
- O terceiro grupo, na minha experiência, o menor, são aqueles programadores que sabem como o código (1-6) funciona e, portanto, podem fazer uma escolha informada.
O risco é possível e qual o tamanho dele, ao usar o código, como nos exemplos acima, depende, como observei anteriormente, do ambiente .

Riscos e suas causas
Os exemplos (1-6) são divididos em dois grupos. O primeiro grupo é um código que bloqueia o segmento de chamada. Este grupo inclui (1-4).
Bloquear um encadeamento geralmente é uma má ideia. Porque Por uma questão de simplicidade, assumimos que todos os encadeamentos são alocados de algum conjunto de encadeamentos. Se o programa tiver um bloqueio, isso poderá levar à seleção de todos os threads do pool. Na melhor das hipóteses, isso desacelerará o programa e levará ao uso ineficiente dos recursos. Na pior das hipóteses, isso pode levar a um conflito, quando um encadeamento adicional é necessário para concluir alguma tarefa, mas o pool não pode alocá-lo.
Assim, quando um desenvolvedor escreve código como (1-4), ele deve pensar sobre a probabilidade da situação descrita acima.
Mas as coisas pioram quando trabalhamos em um ambiente em que há um contexto de sincronização diferente do padrão. Se houver um contexto especial de sincronização, o bloqueio do encadeamento de chamada aumenta a probabilidade de um conflito ocorrer várias vezes. Portanto, o código dos exemplos (1-3), se for executado no thread da interface do usuário WinForms, é quase garantido para criar um conflito. Eu escrevo "praticamente" porque existe uma opção quando isso não é verdade, mas mais sobre isso mais tarde. A adição de ConfigureAwait(false)
, como em (4), não dará 100% de garantia de proteção contra deadlock. A seguir, um exemplo para confirmar isso:
[7] // / . async Task FooAsync() { // Delay . . await Task.Delay(5000); // RestPartOfMethodCode(); } // "" , , WinForms . private void button1_Click(object sender, EventArgs e) { FooAsync() .ConfigureAwait(false) .GetAwaiter() .GetResult(); button1.Text = "new text"; }
O artigo "Computação paralela - tudo sobre o SynchronizationContext" fornece informações sobre vários contextos de sincronização.
Para entender a causa do impasse, você precisa analisar o código da máquina de estado na qual a chamada para o método assíncrono é convertida e, em seguida, o código das classes MS. Um artigo Async Await e Generated StateMachine fornece um exemplo dessa máquina de estado.
Não darei o código fonte completo gerado por exemplo (7), o autômato, mostrarei apenas as linhas importantes para análises posteriores:
// MoveNext. //... // taskAwaiter . taskAwaiter = Task.Delay(5000).GetAwaiter(); if(tasAwaiter.IsCompleted != true) { _awaiter = taskAwaiter; _nextState = ...; _builder.AwaitUnsafeOnCompleted<TaskAwaiter, ThisStateMachine>(ref taskAwaiter, ref this); return; }
A ramificação if
é executada se a chamada assíncrona ( Delay
) ainda não foi concluída e, portanto, o encadeamento atual pode ser liberado.
Observe que no AwaitUnsafeOnCompleted
, taskAwaiter é recebido de uma chamada assíncrona interna (relativa ao FooAsync
) ( Delay
).
Se você mergulhar na selva de fontes MS ocultas atrás da chamada AwaitUnsafeOnCompleted
, no final, chegaremos à classe SynchronizationContextAwaitTaskContinuation e sua classe base AwaitTaskContinuation , onde está a resposta para a pergunta.
O código dessas classes e de outras relacionadas é bastante confuso, portanto, para facilitar a percepção, permito-me escrever um "analógico" muito simplificado do que o exemplo (7) se transforma, mas sem uma máquina de estado e em termos de TPL:
[8] Task FooAsync() { // methodCompleted , , // , " ". // , methodCompleted.WaitOne() , // SetResult AsyncTaskMethodBuilder, // . var methodCompleted = new AutoResetEvent(false); SynchronizationContext current = SynchronizationContext.Current; return Task.Delay(5000).ContinueWith( t=> { if(current == null) { RestPartOfMethodCode(methodCompleted); } else { current.Post(state=>RestPartOfMethodCode(methodCompleted), null); methodCompleted.WaitOne(); } }, TaskScheduler.Current); } // // void RestPartOfMethodCode(AutoResetEvent methodCompleted) // { // FooAsync. // methodCompleted.Set(); // }
No exemplo (8), é importante prestar atenção ao fato de que, se houver um contexto de sincronização, todo o código do método assíncrono que vem após a conclusão da chamada assíncrona interna é executado nesse contexto (chamada current.Post(...)
). Este fato é a causa de impasses. Por exemplo, se estamos falando de um aplicativo WinForms, o contexto de sincronização nele está associado ao fluxo da interface do usuário. Se o fluxo da interface do usuário estiver bloqueado, no exemplo (7) isso acontece por meio de uma chamada para .GetResult()
, o restante do código do método assíncrono não pode ser executado, o que significa que o método assíncrono não pode ser concluído e não pode liberar o fluxo da interface do usuário, que é impasse.
No exemplo (7), a chamada para FooAsync
foi configurada via ConfigureAwait(false)
, mas isso não ajudou. O fato é que você precisa configurar exatamente o objeto de espera que será passado para AwaitUnsafeOnCompleted
. Em nosso exemplo, esse é o objeto de espera da chamada de Delay
. Em outras palavras, nesse caso, chamar ConfigureAwait(false)
no código do cliente não faz sentido. Você pode resolver o problema se o desenvolvedor do método FooAsync
alterar da seguinte maneira:
[9] async Task FooAsync() { await Task.Delay(5000).ConfigureAwait(false); // RestPartOfMethodCode(); } private void button1_Click(object sender, EventArgs e) { FooAsync().GetAwaiter().GetResult(); button1.Text = "new text"; }
Acima, examinamos os riscos que surgem com o código do primeiro grupo - o código com bloqueio (exemplos 1-4). Agora, sobre o segundo grupo (exemplos 5 e 6) - um código sem bloqueios. Nesse caso, a pergunta é: quando é justificada a chamada para ConfigureAwait(false)
? Ao analisar o exemplo (7), já descobrimos que precisamos configurar o objeto em espera com base no qual a continuação da execução será construída. I.e. a configuração é necessária (se você tomar essa decisão) apenas para chamadas assíncronas internas .
Quem é o culpado?
Como sempre, a resposta correta é "tudo". Vamos começar com os programadores da MS. Por um lado, os desenvolvedores da Microsoft decidiram que, na presença de um contexto de sincronização, o trabalho deveria ser realizado por ele. E isso é lógico, caso contrário, por que ainda é necessário. E, como acredito, eles esperavam que os desenvolvedores do código "cliente" não bloqueassem o encadeamento principal, principalmente se o contexto de sincronização estiver vinculado a ele. Por outro lado, eles forneceram uma ferramenta muito simples para "dar um tiro no pé" - é muito simples e conveniente para obter o resultado bloqueando .Result/.GetResult
ou bloquear o fluxo, aguardando o término da chamada, através de .Wait
. I.e. Os desenvolvedores da MS tornaram possível que o uso "errado" (ou perigoso) de suas bibliotecas não cause dificuldades.
Mas também há culpa nos desenvolvedores do código "cliente". Consiste no fato de que, freqüentemente, os desenvolvedores não tentam entender suas ferramentas e negligenciar avisos. E este é um caminho direto para os erros.
O que fazer
Abaixo, dou minhas recomendações.
Para desenvolvedores de código do cliente
- Faça o seu melhor para evitar o bloqueio. Em outras palavras, não misture código síncrono e assíncrono sem necessidade especial.
- Se você precisar fazer um bloqueio, determine em qual ambiente o código é executado:
- Existe um contexto de sincronização? Se sim, qual? Quais recursos ele cria em seu trabalho?
- Se não houver contexto de sincronização, então: Qual será o carregamento? Qual é a probabilidade de seu bloco levar a um "vazamento" de threads do pool? O número de threads criados no início será suficiente por padrão ou devo alocar mais?
- Se o código for assíncrono, você precisará configurar a chamada assíncrona por meio do
ConfigureAwait
?
Tome uma decisão com base em todas as informações recebidas. Pode ser necessário repensar sua abordagem de implementação. Talvez o ConfigureAwait
o ajude ou talvez você não precise dele.
Para desenvolvedores de bibliotecas
- Se você acredita que seu código pode ser chamado de "síncrono", certifique-se de implementar uma API síncrona. Deve ser verdadeiramente síncrono, ou seja, Você deve usar a API síncrona de bibliotecas de terceiros.
ConfigureAwait(true / false)
.
Aqui, do meu ponto de vista, é necessária uma abordagem mais sutil do que o normalmente recomendado. Muitos artigos dizem que no código da biblioteca, todas as chamadas assíncronas devem ser configuradas por meio do ConfigureAwait(false)
. Eu não concordo com isso. Talvez, do ponto de vista dos autores, colegas da Microsoft tenham tomado a decisão errada ao escolher o comportamento "padrão" em relação ao trabalho com o contexto de sincronização. Mas eles (MS), no entanto, deixaram a oportunidade para os desenvolvedores do código "cliente" mudarem esse comportamento. A estratégia, quando o código da biblioteca é completamente coberto pelo ConfigureAwait(false)
, altera o comportamento padrão e, mais importante, essa abordagem priva os desenvolvedores do código de escolha "cliente".
Minha opção é, ao implementar a API assíncrona, adicionar dois parâmetros de entrada adicionais a cada método da API: CancellationToken token
e bool continueOnCapturedContext
. E implemente o código da seguinte maneira:
public async Task<string> FooAsync( /* */, CancellationToken token, bool continueOnCapturedContext) { // ... await Task.Delay(30, token).ConfigureAwait(continueOnCapturedContext); // ... return result; }
O primeiro parâmetro, token
, serve, como você sabe, para a possibilidade de cancelamento coordenado (os desenvolvedores de bibliotecas às vezes negligenciam esse recurso). O segundo, continueOnCapturedContext
- permite configurar a interação com o contexto de sincronização de chamadas assíncronas internas.
Ao mesmo tempo, se o método da API assíncrona fizer parte de outro método assíncrono, o código "cliente" poderá determinar como ele deve interagir com o contexto de sincronização:
// : async Task ClientFoo() { // "" ClientFoo , // FooAsync . await FooAsync( /* */, ancellationToken.None, false); // . await FooAsync( /* */, ancellationToken.None, false).ConfigureAwait(false); //... } // , . private void button1_Click(object sender, EventArgs e) { FooAsync( /* */, _source.Token, false).GetAwaiter().GetResult(); button1.Text = "new text"; }
Em conclusão
A principal conclusão do exposto são os seguintes três pensamentos:
- Os bloqueios costumam ser a raiz de todo mal. É a presença de bloqueios que podem levar, na melhor das hipóteses, à degradação do desempenho e ao uso ineficiente de recursos, na pior das hipóteses - ao impasse. Antes de usar os bloqueios, considere se isso é necessário? Talvez exista outra maneira de sincronização aceitável no seu caso;
- Aprenda a ferramenta com a qual você está trabalhando;
- Se você projetar bibliotecas, tente garantir que o uso correto seja fácil, quase intuitivo e o errado seja repleto de complexidade.
Tentei da maneira mais simples possível explicar os riscos associados ao assíncrono / aguardar e os motivos de sua ocorrência. E também, apresentei minha visão de resolver esses problemas. Espero ter sucesso, e o material será útil para o leitor. Para entender melhor como tudo realmente funciona, você deve, é claro, consultar a fonte. Isso pode ser feito através dos repositórios da MS no GitHub ou, ainda mais conveniente, através do próprio site da MS .
PS Eu ficaria grato por críticas construtivas.
