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()ouTask.Resulte
- 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 comAwaiters.DetachCurrentSyncContext(). Os erros são possíveis aqui, mas sua probabilidade é muito menor.
- O código resultante é mais declarativo e claro. O método ConfigureAwaitcom 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.