Olá Habr! Continuamos a falar sobre programação assíncrona em C #. Hoje falaremos sobre um único caso de uso ou cenário específico do usuário, adequado para qualquer tarefa na estrutura da programação assíncrona. Entraremos em contato com os tópicos de sincronização, deadlocks, configurações do operador, tratamento de exceções e muito mais. Inscreva-se agora!

Artigos relacionados anteriores
Quase todo comportamento não padrão de métodos assíncronos em C # pode ser explicado com base em um cenário de usuário: a conversão de um código síncrono existente em assíncrono deve ser o mais simples possível. Você deve poder adicionar a palavra-chave async antes do tipo de retorno do método, adicionar o sufixo Async ao nome desse método e adicionar a palavra-chave wait aqui e na área de texto do método para obter um método assíncrono totalmente funcional.

Um cenário "simples" altera drasticamente muitos aspectos do comportamento dos métodos assíncronos: do planejamento da duração de uma tarefa ao tratamento de exceções. O script parece convincente e significativo, mas em seu contexto, a simplicidade dos métodos assíncronos se torna muito enganadora.
Contexto de sincronização
O desenvolvimento da interface do usuário (UI) é uma área em que o cenário acima é especialmente importante. Devido a longas operações no encadeamento da interface com o usuário, o tempo de resposta dos aplicativos aumenta; nesse caso, a programação assíncrona sempre foi considerada uma ferramenta muito eficaz.
private async void buttonOk_ClickAsync(object sender, EventArgs args) { textBox.Text = "Running..";
O código parece muito simples, mas há um problema. Existem restrições para a maioria das interfaces de usuário: os elementos da interface do usuário podem ser alterados apenas por threads especiais. Ou seja, na linha 3, ocorre um erro se a duração da tarefa estiver agendada no encadeamento do pool de encadeamentos. Felizmente, esse problema é conhecido há muito tempo e o conceito de
um contexto de sincronização apareceu na versão do .NET Framework 2.0.
Cada interface do usuário fornece utilitários especiais para organizar tarefas em um ou mais encadeamentos especializados da interface com o usuário. O Windows Forms usa o método
Control.Invoke
, o WPF
Control.Invoke
método Dispatcher.Invoke, outros sistemas podem acessar outros métodos. Os esquemas usados em todos esses casos são amplamente semelhantes, mas diferem em detalhes. O contexto de sincronização permite abstrair das diferenças fornecendo uma API para executar o código em um contexto "especial" que fornece o processamento de detalhes secundários por tipos derivados, como
WindowsFormsSynchronizationContext
,
DispatcherSynchronizationContext
, etc.
Para resolver o problema de afinidade do encadeamento, os programadores de C # decidiram entrar no contexto atual de sincronização no estágio inicial da implementação dos métodos assíncronos e planejar todas as operações subseqüentes nesse contexto. Agora, cada um dos blocos entre as instruções wait é executado no encadeamento da interface do usuário, o que possibilita a implementação do script principal. No entanto, essa solução deu origem a vários novos problemas.
Deadlocks
Vejamos um pequeno pedaço de código relativamente simples. Há algum problema aqui?
Este código causa um
conflito . O encadeamento da interface com o usuário inicia uma operação assíncrona e aguarda o resultado de forma síncrona. No entanto, o método assíncrono não pode ser concluído porque a segunda linha de
GetStockPricesForAsync
deve ser executada no thread da interface do usuário que causa o conflito.
Você objetará que esse problema é bastante fácil de resolver. Sim de fato. Você precisa proibir todas as chamadas para o
Task.Wait
ou
Task.Wait
do código da interface do usuário; no entanto, o problema ainda pode ocorrer se o componente usado por esse código estiver aguardando o resultado da operação do usuário de forma síncrona:
Esse código novamente causa um impasse. Como resolver:
- Você não deve bloquear o código assíncrono com
Task.Wait()
ou Task.Result
e - use
ConfigureAwait(false)
no código da biblioteca.
O significado da primeira recomendação é claro, e a segunda explicaremos abaixo.
Configurando instruções de espera
Há duas razões pelas quais um conflito ocorre no último exemplo:
Task.Wait()
em
GetStockPricesForAsync
e uso indireto do contexto de sincronização nas etapas subseqüentes em InitializeIfNeededAsync. Embora os programadores de C # não recomendem o bloqueio de chamadas para métodos assíncronos, é óbvio que na maioria dos casos esse bloqueio ainda é usado. Os programadores de C # oferecem a seguinte solução para um problema de
Task.ConfigureAwait(continueOnCapturedContext:false)
:
Task.ConfigureAwait(continueOnCapturedContext:false)
.
Apesar da aparência estranha (se uma chamada de método é executada sem um argumento nomeado, isso não significa nada), esta solução executa sua função: fornece uma continuação forçada da execução sem um contexto de sincronização.
public Task<decimal> GetStockPricesForAsync(string symbol) { InitializeIfNeededAsync().Wait(); return Task.FromResult((decimal)42); } private async Task InitializeIfNeededAsync() => await Task.Delay(1).ConfigureAwait(false);
Nesse caso, a continuação da tarefa
Task.Delay(1
) (aqui está a instrução vazia) é planejada no encadeamento do pool de encadeamentos, e não no encadeamento da interface do usuário, que elimina o impasse.
Desativando o contexto de sincronização
Eu sei que o
ConfigureAwait
realmente resolve esse problema, mas gera muito mais. Aqui está um pequeno exemplo:
public Task<decimal> GetStockPricesForAsync(string symbol) { InitializeIfNeededAsync().Wait(); return Task.FromResult((decimal)42); } private async Task InitializeIfNeededAsync() {
Você vê o problema? Usamos o
ConfigureAwait(false)
, portanto tudo deve ficar bem. Mas não é um fato.
ConfigureAwait(false)
retorna um objeto
ConfiguredTaskAwaitable
garçom personalizado e sabemos que ele será usado apenas se a tarefa não for concluída de forma síncrona. Ou seja, se
_cache.InitializeAsync()
terminar de forma síncrona, um conflito ainda será possível.
Para eliminar conflitos, todas as tarefas que aguardam conclusão devem ser "decoradas" com uma chamada para o método
ConfigureAwait(false)
. Tudo isso irrita e gera erros.
Como alternativa, você pode usar o objeto personalizado do garçom em todos os métodos públicos para desativar o contexto de sincronização no método assíncrono:
private void buttonOk_Click(object sender, EventArgs args) { textBox.Text = "Running.."; var result = _stockPrices.GetStockPricesForAsync("MSFT").Result; textBox.Text = "Result is: " + result; }
Awaiters.DetachCurrentSyncContext
retorna o seguinte objeto de garçom personalizado:
public struct DetachSynchronizationContextAwaiter : ICriticalNotifyCompletion {
DetachSynchronizationContextAwaiter
faz o seguinte: o método async funciona com um contexto de sincronização diferente de zero. Mas se o método assíncrono funcionar sem um contexto de sincronização, a propriedade
IsCompleted
retornará true e a continuação do método será realizada de forma síncrona.
Isso significa que os dados do serviço estão próximos de zero quando o método assíncrono é executado a partir de um encadeamento no conjunto de encadeamentos e o pagamento é feito uma vez para transferir a execução do encadeamento da interface com o usuário para o encadeamento do conjunto de encadeamentos.
Outros benefícios dessa abordagem estão listados abaixo.
- A probabilidade de erro é reduzida.
ConfigureAwait(false)
funciona apenas se aplicado a todas as tarefas que aguardam conclusão. Vale a pena esquecer pelo menos uma coisa - e pode ocorrer um impasse. No caso de um objeto de garçom personalizado, lembre-se de que todos os métodos de biblioteca pública devem começar com Awaiters.DetachCurrentSyncContext()
. Os erros são possíveis aqui, mas sua probabilidade é muito menor. - O código resultante é mais declarativo e claro. O método
ConfigureAwait
com várias chamadas parece menos legível para mim (devido a elementos extras) e não é informativo o suficiente para iniciantes.
Manipulação de exceção
Qual é a diferença entre essas duas opções:
Tarefa mayFail = Task.FromException (new ArgumentNullException ());
No primeiro caso, tudo atende às expectativas - o processamento de erros é realizado, mas no segundo caso isso não acontece. A biblioteca de tarefas paralelas TPL foi projetada para programação assíncrona e paralela, e Tarefa / Tarefa pode representar o resultado de várias operações. É por isso que
Task.Result
e
Task.Wait()
sempre lançam um
AggregateException
, que pode conter vários erros.
No entanto, nosso cenário principal muda tudo: o usuário deve poder adicionar o operador assíncrono / aguardar sem tocar na lógica de tratamento de erros. Ou seja, a instrução de espera deve ser diferente de
Task.Result
/
Task.Wait()
: deve remover o wrapper de uma exceção na instância
AggregateException
. Hoje vamos selecionar a primeira exceção.
Tudo ficará bem se todos os métodos baseados em Tarefa forem assíncronos e cálculos paralelos não forem usados para executar tarefas. Mas, em alguns casos, tudo é diferente:
try { Task<int> task1 = Task.FromException<int>(new ArgumentNullException()); Task<int> task2 = Task.FromException<int>(new InvalidOperationException());
Task.WhenAll
retorna uma tarefa com dois erros, no entanto, a instrução de espera recupera e preenche apenas o primeiro.
Existem duas maneiras de resolver esse problema:
- exibir manualmente as tarefas se elas tiverem acesso ou
- configure a biblioteca TPL para forçar a exceção a ser quebrada em outra
AggregateException
.
try { Task<int> task1 = Task.FromException<int>(new ArgumentNullException()); Task<int> task2 = Task.FromException<int>(new InvalidOperationException());
Método de vazio assíncrono
O método baseado em tarefas retorna um token que pode ser usado para processar resultados no futuro. Se a tarefa for perdida, o token se tornará inacessível para leitura pelo código do usuário. Uma operação assíncrona que retorna o método void gera um erro que não pode ser tratado no código do usuário. Nesse sentido, os tokens são inúteis e até perigosos - agora vamos vê-lo. No entanto, nosso cenário principal assume seu uso obrigatório:
private async void buttonOk_ClickAsync(object sender, EventArgs args) { textBox.Text = "Running.."; var result = await _stockPrices.GetStockPricesForAsync("MSFT"); textBox.Text = "Result is: " + result; }
Mas e se
GetStockPricesForAsync
um erro? Uma exceção de método nulo assíncrono não tratado é empacotada no contexto de sincronização atual, disparando o mesmo comportamento do código síncrono (para obter mais informações, consulte o
método ThrowAsync na página da web
AsyncMethodBuilder.cs ). No Windows Forms, uma exceção não tratada no manipulador de eventos dispara o evento
Application.ThreadException
, para WPF, o evento
Application.DispatcherUnhandledException
acionado e assim por diante.
E se o método async void não obtiver o contexto de sincronização? Nesse caso, uma exceção não tratada causa uma falha fatal no aplicativo. Ele não dispara o evento [
TaskScheduler.UnobservedTaskException
] que será recuperado, mas dispara o evento irrecuperável
AppDomain.UnhandledException
e fecha o aplicativo. Isso acontece intencionalmente, e este é exatamente o resultado que precisamos.
Agora, vamos ver outra maneira bem conhecida: usar métodos void assíncronos apenas para manipuladores de eventos da interface do usuário.
Infelizmente, o método de nulo assíncrono é fácil de chamar por acidente.
public static Task<T> ActionWithRetry<T>(Func<Task<T>> provider, Action<Exception> onError) {
À primeira vista, a expressão lambda é difícil de dizer se a função é um método baseado em tarefas ou um método de vazio assíncrono e, portanto, um erro pode surgir na sua base de código, apesar da verificação mais completa.
Conclusão
Muitos aspectos da programação assíncrona em C # foram influenciados por um cenário de usuário único - simplesmente convertendo o código síncrono de um aplicativo de interface do usuário existente em assíncrono:
- A execução subsequente de métodos assíncronos é agendada no contexto de sincronização resultante, o que pode causar conflitos.
- Para evitá-los, é necessário fazer chamadas
ConfigureAwait(false)
em qualquer lugar do código da biblioteca assíncrona. - aguardar tarefa; produz o primeiro erro e isso complica a criação de uma exceção de processamento para programação paralela.
- Os métodos Async void foram introduzidos para lidar com eventos da interface do usuário, mas são fáceis de executar acidentalmente, o que causará uma falha no aplicativo se uma exceção for lançada.
Queijo grátis acontece apenas em uma ratoeira. A facilidade de uso às vezes pode levar a grandes dificuldades em outras áreas. Se você está familiarizado com o histórico de programação assíncrona em C #, o comportamento mais estranho não parece mais tão estranho e a probabilidade de erros no código assíncrono é significativamente reduzida.