Em poucas palavras: Async / Await Best Practices in .NET

Antecipando o início do curso, "C # Developer" preparou uma tradução de material interessante.




Async / Await - Introdução


A construção da linguagem Async / Await existe desde o C # versão 5.0 (2012) e rapidamente se tornou um dos pilares da moderna programação .NET - qualquer desenvolvedor de C # que se preze deveria usá-lo para melhorar o desempenho do aplicativo, a capacidade de resposta geral e a legibilidade do código.

O Async / Await simplifica a introdução de códigos assíncronos e elimina a necessidade de o programador entender os detalhes de seu processamento, mas quantos de nós realmente sabemos como ele funciona e quais são as vantagens e desvantagens desse método? Há muitas informações úteis, mas são fragmentadas, então decidi escrever este artigo.

Bem, então, vamos nos aprofundar no tópico.

Máquina de estado (IAsyncStateMachine)


A primeira coisa que você precisa saber é que sempre que você tem um método ou função com o Async / Await, o compilador realmente transforma seu método em uma classe gerada que implementa a interface IAsyncStateMachine. Essa classe é responsável por manter o estado do seu método durante o ciclo de vida de uma operação assíncrona - encapsula todas as variáveis ​​do seu método na forma de campos e divide seu código em seções que são executadas durante transições de máquina de estado entre estados, para que o thread possa sair do método e quando retornará, o estado não mudará.

Como exemplo, aqui está uma definição de classe muito simples com dois métodos assíncronos:

using System.Threading.Tasks;

using System.Diagnostics;

namespace AsyncAwait
{
    public class AsyncAwait
    {

        public async Task AsyncAwaitExample()
        {
            int myVariable = 0;

            await DummyAsyncMethod();
            Debug.WriteLine("Continuation - After First Await");
            myVariable = 1;

            await DummyAsyncMethod();
            Debug.WriteLine("Continuation - After Second Await");
            myVariable = 2;

        }

        public async Task DummyAsyncMethod()
        {
            // 
        }

    }
}

Uma classe com dois métodos assíncronos

Se observarmos o código gerado durante a montagem, veremos algo assim:



Observe que temos 2 novas classes internas geradas para nós, uma para cada método assíncrono. Essas classes contêm uma máquina de estado para cada um dos nossos métodos assíncronos.

Além disso, tendo estudado o código descompilado para <AsyncAwaitExample> d__0, notamos que nossa variável interna «myVariable»agora é um campo de classe:



também podemos ver outros campos de classe usados ​​internamente para manter o estado IAsyncStateMachine. A máquina de estados passa por estados usando o métodoMoveNext(), de fato, um interruptor grande. Observe como o método continua em seções diferentes após cada uma das chamadas assíncronas (com o rótulo de continuação anterior).



Isso significa que a elegância assíncrona / aguardada tem um preço. O uso de async / waitit realmente adiciona alguma complexidade (da qual você talvez não esteja ciente). Na lógica do servidor, isso pode não ser crítico, mas, em particular, ao programar aplicativos móveis que levam em consideração cada ciclo de CPU e de memória KB, lembre-se disso, pois a quantidade de sobrecarga pode aumentar rapidamente. Posteriormente neste artigo, discutiremos as práticas recomendadas para usar o Async / Await somente quando necessário.

Para uma explicação bastante instrutiva da máquina de estado, assista a este vídeo no YouTube.

Quando usar o Async / Await


Geralmente, existem dois cenários em que o Async / Await é a solução certa.

  • Trabalho relacionado à E / S : seu código espera algo, como dados de um banco de dados, leitura de um arquivo, chamada de um serviço da web. Nesse caso, você deve usar Async / Await, não a Biblioteca Paralela de Tarefas.
  • Trabalho relacionado à CPU : seu código executará cálculos complexos. Nesse caso, você deve usar Async / Await, mas precisa iniciar o trabalho em outro thread usando o Task.Run. Você também pode considerar usar a Biblioteca Paralela de Tarefas .



Assíncrono todo o caminho


Ao começar a trabalhar com métodos assíncronos, você notará rapidamente que a natureza assíncrona do código começa a se espalhar pela hierarquia de chamadas - isso significa que você também deve tornar o código de chamada assíncrono e assim por diante.
Você pode ficar tentado a "parar" isso bloqueando o código usando Task.Result ou Task.Wait, convertendo uma pequena parte do aplicativo e envolvendo-o em uma API síncrona para que o restante do aplicativo fique isolado das alterações. Infelizmente, esta é uma receita para criar impasses difíceis de rastrear.

A melhor solução para esse problema é permitir que o código assíncrono cresça naturalmente na base de código. Se você seguir esta decisão, verá a extensão do código assíncrono em seu ponto de entrada, geralmente um manipulador de eventos ou uma ação do controlador. Renda-se à assincronia sem deixar rastro!

Mais informações neste artigo do MSDN.

Se o método for declarado como assíncrono, certifique-se de que esteja aguardando!


Como discutimos, quando o compilador encontra um método assíncrono, ele transforma esse método em uma máquina de estado. Se o seu código não esperar no corpo, o compilador gerará um aviso, mas a máquina de estado será criada, adicionando sobrecarga desnecessária para uma operação que nunca será concluída.

Evitar vazio assíncrono


O vácuo assíncrono é algo que realmente deve ser evitado. Torne uma regra usar a tarefa assíncrona em vez do vazio assíncrono.

public async void AsyncVoidMethod()
{
    //!
}

public async Task AsyncTaskMethod()
{
    //!
}

Os métodos async void e async Task

Existem vários motivos para isso, incluindo:

  • Exceções lançadas no método assíncrono nulo não podem ser capturadas fora deste método :
Quando uma exceção é lançada do método Tarefa assíncrona ou Tarefa assíncrona <T >, essa exceção é capturada e colocada no objeto Tarefa. Ao usar métodos async void, o objeto Task está ausente; portanto, quaisquer exceções lançadas no método async void serão chamadas diretamente no SynchronizationContext, que estava ativo quando o método async void foi iniciado.

Considere o exemplo abaixo. O bloco de captura nunca será alcançado.

public async void AsyncVoidMethodThrowsException()
{
    throw new Exception("Hmmm, something went wrong!");
}

public void ThisWillNotCatchTheException()
{
    try
    {
        AsyncVoidMethodThrowsException();
    }
    catch(Exception ex)
    {
        //     
        Debug.WriteLine(ex.Message);
    }
}

Exceções lançadas no método async void não podem ser capturadas fora deste

método.Compare com este código, onde, em vez de async void, temos a tarefa assíncrona. Nesse caso, a captura será alcançável.

public async Task AsyncTaskMethodThrowsException()
{
    throw new Exception("Hmmm, something went wrong!");
}

public async Task ThisWillCatchTheException()
{
    try
    {
        await AsyncTaskMethodThrowsException();
    }
    catch (Exception ex)
    {
        //    
        Debug.WriteLine(ex.Message);
    }
}

A exceção é capturada e colocada no objeto Tarefa.

  • Os métodos assíncronos nulos podem causar efeitos colaterais indesejados se o chamador não esperar que eles sejam assíncronos : se o método assíncrono não retornar nada, use a tarefa assíncrona (sem um " <T >" para a tarefa) como tipo de retorno.
  • Os métodos assíncronos nulos são muito difíceis de testar : devido a diferenças no tratamento e layout de erros, é difícil escrever testes de unidade que chamam métodos assíncronos nulos. Teste Asynchronous MSTest só funciona para métodos assíncronos que retornam um Task ou Task <T >.

Uma exceção a essa prática são manipuladores de eventos assíncronos. Mas mesmo neste caso, é recomendável minimizar o código escrito no próprio manipulador - espere um método de tarefa assíncrona que contenha lógica.

Mais informações neste artigo do MSDN.

Preferir tarefa de retorno em vez de retorno aguardar


Como já discutido, toda vez que você declara um método como assíncrono, o compilador cria uma classe de máquina de estado que realmente envolve a lógica do seu método. Isso adiciona certas despesas gerais que podem se acumular, especialmente para dispositivos móveis, onde temos limites de recursos mais rigorosos.

Às vezes, um método não precisa ser assíncrono, mas retorna a Tarefa <T >e permite que o outro lado o manipule adequadamente. Se a última frase do seu código for um retorno aguardado, considere refatorá-lo para que o tipo de retorno do método seja Tarefa <T>(em vez de assíncrono T). Por isso, você evita gerar uma máquina de estado, o que torna seu código mais flexível. O único caso que realmente queremos esperar é quando fazemos algo com o resultado da tarefa assíncrona na continuação do método.

public async Task<string> AsyncTask()

{
   //  !
   //...  -  
   //await -   ,  await  

   return await GetData();

}

public Task<string> JustTask()

{
   //!
   //...  -  
   // Task

   return GetData();

}

Preferir retornar tarefa em vez de retornar aguardar

Observe que se não tivermos aguardado e retornar a tarefa <T >, o retorno ocorrerá imediatamente; portanto, se o código estiver dentro de um bloco try / catch, a exceção não será capturada. Da mesma forma, se o código estiver dentro do bloco using, ele excluirá imediatamente o objeto. Veja a próxima dica.

Não coloque a tarefa de retorno dentro de try..catch {} ou usando {} blocos


A Tarefa de Retorno pode causar comportamento indefinido quando usada dentro de um bloco try..catch (uma exceção lançada pelo método assíncrono nunca será capturada) ou dentro de um bloco using, porque a tarefa será retornada imediatamente.

Se você precisar agrupar seu código assíncrono em uma tentativa .. captura ou usando o bloco, use return wait aguardar.

public Task<string> ReturnTaskExceptionNotCaught()

{
   try
   {
       // ...

       return GetData();

   }
   catch (Exception ex)

   {
       //     

       Debug.WriteLine(ex.Message);
       throw;
   }

}

public Task<string> ReturnTaskUsingProblem()

{
   using (var resource = GetResource())
   {

       // ...  ,     , ,    

       return GetData(resource);
   }
}

Não envolva a tarefa de retorno dentro de blocos try..catch{}ouusing{} .

Mais informações neste tópico sobre estouro de pilha.

Evite usar .Wait()ou .Result- use em vez dissoGetAwaiter().GetResult()


Se você precisar bloquear a espera pela conclusão da tarefa assíncrona, use GetAwaiter().GetResult(). Waite Resultlance quaisquer exceções AggregateException, o que complica o tratamento de erros. A vantagem GetAwaiter().GetResult()é que ele retorna a exceção usual AggregateException.

public void GetAwaiterGetResultExample()

{
   // ,    ,     AggregateException  

   string data = GetData().Result;

   // ,   ,      

   data = GetData().GetAwaiter().GetResult();
}

Se você precisar bloquear a espera pela conclusão da tarefa assíncrona, use as GetAwaiter().GetResult().

informações adicionais neste link .

Se o método for assíncrono, adicione o sufixo Async ao seu nome


Essa é a convenção usada no .NET para distinguir mais facilmente métodos síncronos e assíncronos (com exceção dos manipuladores de eventos ou métodos do controlador da web, mas eles ainda não devem ser explicitamente chamados pelo seu código).

Os métodos de biblioteca assíncrona devem usar Task.ConfigureAwait (false) para melhorar o desempenho



O .NET Framework tem o conceito de um "contexto de sincronização", que é uma maneira de "voltar para onde você estava antes". Sempre que uma tarefa está aguardando, ela captura o contexto de sincronização atual antes de aguardar.

Após a conclusão da tarefa .Post(), o método de contexto de sincronização é chamado , que retoma o trabalho de onde estava antes. Isso é útil para retornar ao thread da interface do usuário ou para o mesmo contexto do ASP.NET etc.
Ao escrever o código da biblioteca, você raramente precisa voltar ao contexto em que estava antes. Quando Task.ConfigureAwait (false) é usado, o código não tenta mais continuar de onde estava antes; em vez disso, se possível, o código sai no encadeamento que concluiu a tarefa, o que evita a alternância de contexto. Isso melhora um pouco o desempenho e pode ajudar a evitar conflitos.

public async Task ConfigureAwaitExample()

{
   //   ConfigureAwait(false)   .

   var data = await GetData().ConfigureAwait(false);
}

Normalmente, use ConfigureAwait (false) para processos do servidor e código da biblioteca.
Isso é especialmente importante quando o método da biblioteca é chamado um grande número de vezes, para uma melhor capacidade de resposta.

Normalmente, use ConfigureAwait (false) para processos do servidor em geral. Não nos importamos com qual thread é usado para continuar, ao contrário dos aplicativos em que precisamos retornar ao thread da interface do usuário.

Agora ... No ASP.NET Core, a Microsoft acabou com o SynchronizationContext, portanto, teoricamente, você não precisa disso. Mas se você escrever um código de biblioteca que possa ser reutilizado em outros aplicativos (por exemplo, aplicativo de interface do usuário, ASP.NET herdado, Xamarin Forms), isso continuará sendo uma prática recomendada .

Para uma boa explicação desse conceito, assista a este vídeo .

Relatório de andamento da tarefa assíncrona


Um caso de uso bastante comum para métodos assíncronos é trabalhar em segundo plano, liberar o thread da interface do usuário para outras tarefas e manter a capacidade de resposta. Nesse cenário, convém relatar o progresso de volta à interface com o usuário para que ele possa monitorar o progresso do processo e interagir com a operação.

Para resolver esse problema comum, o .NET fornece a interface IProgress <T >, que fornece o método Report <T >, invocado por uma tarefa assíncrona para relatar o progresso ao chamador. Essa interface é aceita como um parâmetro do método assíncrono - o chamador deve fornecer um objeto que implemente essa interface.

O .NET fornece o Progress <T >, a implementação padrão do IProgress <T >, que é realmente recomendada, pois lida com toda a lógica de baixo nível associada ao salvamento e restauração do contexto de sincronização. O Progress <T >também fornece um evento da Ação <T e um retorno de chamada >- ambos chamados quando uma tarefa relata o progresso.

Juntos, o IProgress <T >e o Progress <T >fornecem uma maneira fácil de transferir informações de progresso de uma tarefa em segundo plano para um thread da interface do usuário.

Por favor, note que <T>pode ser um valor simples, como um int, ou um objeto que forneça informações contextuais sobre o progresso, como a porcentagem de conclusão, uma descrição de cadeia de caracteres da operação atual, ETA e assim por diante.
Considere com que frequência você relata o progresso. Dependendo da operação que você está executando, você pode descobrir que seus relatórios de código progridem várias vezes por segundo, o que pode resultar na interface do usuário se tornar menos responsiva. Nesse cenário, recomenda-se que o progresso seja relatado em intervalos maiores.

Mais informações neste artigo no blog oficial do Microsoft .NET.

Cancelar tarefas assíncronas


Outro caso de uso comum para tarefas em segundo plano é a capacidade de cancelar a execução. O .NET fornece a classe CancellationToken. O método assíncrono recebe o objeto CancellationToken, que é então compartilhado pelo código da parte que chama e pelo método assíncrono, fornecendo um mecanismo para sinalizar o cancelamento.

No caso mais comum, o cancelamento ocorre da seguinte maneira:

  1. O chamador cria um objeto CancellationTokenSource.
  2. O chamador chama a API assíncrona cancelada e passa o CancellationToken do CancellationTokenSource (CancellationTokenSource.Token).
  3. O chamador solicita um cancelamento usando o objeto CancellationTokenSource (CancellationTokenSource.Cancel ()).
  4. A tarefa confirma o cancelamento e cancela a si mesma, geralmente usando o método CancellationToken.ThrowIfCancellationRequested.

Observe que, para que esse mecanismo funcione, você precisará escrever um código para verificar os cancelamentos solicitados em intervalos regulares (ou seja, a cada iteração do seu código ou em um ponto de interrupção natural na lógica). Idealmente, após uma solicitação de cancelamento, a tarefa assíncrona deve ser cancelada o mais rápido possível.

Você deve considerar desfazer todos os métodos que podem levar muito tempo para serem concluídos.

Mais informações neste artigo no blog oficial do Microsoft .NET.

Relatório de andamento e cancelamento - exemplo


using System;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Threading;
namespace TestAsyncAwait
{
   public partial class AsyncProgressCancelExampleForm : Form
   {
       public AsyncProgressCancelExampleForm()
       {
           InitializeComponent();
       }

       CancellationTokenSource _cts = new CancellationTokenSource();

       private async void btnRunAsync_Click(object sender, EventArgs e)

       {

           //   .

            <int>   ,          ,   ,    , ETA  . .

           var progressIndicator = new Progress<int>(ReportProgress);

           try

           {
               //   ,         

               await AsyncMethod(progressIndicator, _cts.Token);

           }

           catch (OperationCanceledException ex)

           {
               // 

               lblProgress.Text = "Cancelled";
           }
       }

       private void btnCancel_Click(object sender, EventArgs e)

       {
          // 
           _cts.Cancel();

       }

       private void ReportProgress(int value)

       {
           //    

           lblProgress.Text = value.ToString();

       }

       private async Task AsyncMethod(IProgress<int> progress, CancellationToken ct)

       {

           for (int i = 0; i < 100; i++)

           {
              //   ,     

               await Task.Delay(1000);

               //   

               if (ct != null)

               {

                   ct.ThrowIfCancellationRequested();

               }

               //   

               if (progress != null)

               {

                   progress.Report(i);
               }
           }
       }
   }
}

Aguardando um período de tempo


Se você precisar esperar um pouco (por exemplo, tente novamente verificar a disponibilidade do recurso), use Task.Delay - nunca use Thread.Sleep nesse cenário.

Aguardando a conclusão de várias tarefas assíncronas


Use Task.WaitAny para aguardar a conclusão de qualquer tarefa. Use Task.WaitAll para aguardar a conclusão de todas as tarefas.

Preciso me apressar para mudar para C # 7 ou 8? Inscreva-se em um seminário on - line gratuito para discutir este tópico.

All Articles