C# - Boas práticas para usar com async e await


  Hoje veremos boas práticas associadas com o uso da programação assíncrona usando async e await.

No mundo do desenvolvimento de software moderno, escrever código assíncrono é essencial para garantir que seus aplicativos sejam responsivos e escaláveis.

A plataforma .NET Core fornece ferramentas poderosas para gerenciar operações assíncronas usando as palavras-chave async e await, e , veremos práticas recomendadas para usar async/await em C# com .NET Core, começando com exemplos simples e avançando gradualmente para cenários mais complexos.

Por que usar Async/Await?

Antes de mergulhar nas práticas recomendadas, vamos entender brevemente por que usamos async/await em C#:

- Ter Interfaces de usuário responsivas: O código assíncrono permite que a interface de usuário do seu aplicativo permaneça responsiva, mesmo ao executar operações demoradas, como consultas de banco de dados ou solicitações da web.

- Obter Escalabilidade aprimorada: As operações assíncronas permitem lidar com eficiência com um grande número de tarefas simultâneas, tornando seu aplicativo mais escalonável.

- Garantir eficiência de recursos: Ao evitar o bloqueio de chamadas, o código assíncrono permite que threads sejam reutilizados de forma eficiente, reduzindo o consumo de recursos.

- Melhorar experiência do usuário: A execução mais rápida de tarefas de longa duração resulta em uma melhor experiência geral do usuário.

Boas práticas para async e await

1- Use async sempre que for possível e pertinente

Sempre que você encontrar operações vinculadas a operações E/S, como consultas de banco de dados, solicitações de rede ou E/S de arquivo, considere tornar o método assíncrono.

Por exemplo:

1- Obter dados do site jsonplaceholder

Console.WriteLine("Pressione algo para obter os dados ");
Console
.ReadKey();

await FazerRequisicaoAsync();

Console.ReadLine();

static async Task FazerRequisicaoAsync()
{
  
// Crie uma instância do HttpClient
  
using (var httpClient = new HttpClient())
   {
     
try
      {
        
// Especifique a URL que você deseja acessar.
        
string url = "https://jsonplaceholder.typicode.com/posts/1";
        
// Faça uma solicitação GET de forma assíncrona e aguarde a resposta.
        
HttpResponseMessage response = await httpClient.GetAsync(url);
        
// Verifique se a resposta foi bem-sucedida (código de status 200 OK).
        
if (response.IsSuccessStatusCode)
         {
           
// Leia o conteúdo da resposta de forma assíncrona.
           
string conteudo = await response.Content.ReadAsStringAsync();
           
// Exiba o conteúdo da página.
           
Console.WriteLine(conteudo);
          }

          else

          {
           
Console.WriteLine("A solicitação não foi bem-sucedida. Código de status: "
                               + response.StatusCode);
          }
       }
      
catch (HttpRequestException e)
       {
         
Console.WriteLine("Erro na solicitação HTTP: " + e.Message);
       }
     }
 }

- Neste exemplo, criamos o método FazerRequisicaoAsync() como um método assíncrono que simula uma solicitação HTTP usando HttpClient.
- Usamos await para aguardar de forma assíncrona a leitura da resposta HTTP e do conteúdo.
- Chamamos
FazerRequisicaoAsync() de forma assíncrona, imprimindo o resultado no console.

Aqui está outro exemplo simples de como um método assíncrono pode ser útil em uma operação de E/S:

public async Task<string> LerArquivoAsync(string caminhoArquivo)
{
  
using (var leitor = new StreamReader(caminhoArquivo))
   {
     
return await leitor.ReadToEndAsync();
   }
}

2- Use ConfigureAwait(false) para threads que não tratam com UI

o método ConfigureAwait(false) é uma opção que você pode usar para controlar o contexto de sincronização no qual o código assíncrono é executado. Isso é útil quando você deseja evitar que o código continue em um contexto específico após uma operação assíncrona ser concluída, como um contexto de thread específico (por exemplo, o contexto da thread principal da interface do usuário).

Quando você utiliza o await em uma operação assíncrona, o código após o await pode continuar a ser executado no mesmo contexto de sincronização do qual a operação assíncrona estava sendo chamada. Isso é importante em cenários nos quais você deseja manter o contexto atual, como a UI thread em aplicativos Windows Forms ou WPF, onde as atualizações de interface do usuário devem ser feitas no contexto da thread principal.

No entanto, em algumas situações, você pode querer evitar o contexto de sincronização e permitir que o código assíncrono continue em qualquer contexto disponível.

É aí que o ConfigureAwait(false) entra em jogo.

Quando você utiliza ConfigureAwait(false), você está dizendo ao runtime para não tentar continuar a execução no contexto original de sincronização após a conclusão da operação assíncrona.

processá-la.

public async Task MeuMetodoAsync()
{
  
// O código antes do await é executado no contexto atual.

  
await MinhaOperacao().ConfigureAwait(false);

  
// O código após o await não será executado no contexto original de sincronização.
}
 

Neste exemplo, o código após o await com ConfigureAwait(false) não será executado no contexto original de sincronização, o que pode ser útil em cenários onde você deseja evitar bloqueios ou quando você está escrevendo código em bibliotecas que não deveriam assumir nada sobre o contexto de sincronização.

É importante observar que o uso de ConfigureAwait(false) pode melhorar o desempenho em cenários de I/O, onde o contexto de sincronização pode ser desnecessário e causar overhead. No entanto, ao usar ConfigureAwait(false), você deve ter cuidado para não causar problemas em cenários de interface do usuário, onde a atualização da UI deve ser feita no contexto da thread principal.

Certifique-se de usá-lo com discernimento e compreensão das implicações do contexto de sincronização em seu código.

3- Evite usar async void

Você deve evitar o uso de async void na maioria dos cenários em C# porque ele tem implicações significativas e pode levar a problemas de gerenciamento de exceções e controle de fluxo.

Aqui estão algumas razões para evitar o uso de async void:

- Dificuldades no tratamento de exceções: Quando um método async retorna void, você perde a capacidade de aguardar a conclusão da operação assíncrona usando await.

- Falta de feedback sobre a conclusão: Métodos async que retornam Task ou Task<T> permitem que você saiba quando a operação assíncrona foi concluída;

- Dificuldades de teste: Quando um método é async void, pode ser complicado testá-lo, pois não há um mecanismo direto para aguardar a conclusão da operação. Isso pode dificultar a criação de testes unitários robustos.

- Comportamento imprevisível em aplicativos de interface do usuário: Em aplicativos de interface do usuário (como WPF ou Windows Forms), o uso de async void pode levar a problemas, como bloqueio da interface do usuário ou falhas não tratadas, quando exceções são lançadas

Exemplo:

public class ExemploAsync
{
    // Define um evento
    public event EventHandler<string> TarefaConcluida;

    public async Task RealizarOperacaoAsync()
    {
        await Task.Delay(1000);
        Console.WriteLine("Tarefa Concluída.");

        // Dispara o evento quando a operação for completada
        TarefaConcluida?.Invoke(this, "A Tarefa foi concluída.");
    }

    public async void TratarEventoAsync()
    {
        await RealizarOperacaoAsync();
    }

    public static async Task Main(string[] args)
    {
        var example = new ExemploAsync();

        // Se inscreve no evento
        example.TarefaConcluida += (sender, message) =>
        {
            Console.WriteLine("Event handler: " + message);
        };

        //boa prática
        await example.RealizarOperacaoAsync();
        //somente para tratamento de eventos assincronos
        example.TratarEventoAsync();  
    }
}

Entendendo o código:

Definimos um evento chamado TarefaConcluida usando o delegado EventHandler<string>. Este evento será gerado quando a operação assíncrona for concluída e carrega uma mensagem como parâmetro de string.

Aqui mostramos o uso de async void  para o  manipuladores de eventos assíncronos que é um cenário onde ele pode ser usado.

No método
RealizarOperacaoAsync, realizamos uma operação assíncrona com atraso para simular o trabalho que está sendo realizado. Após a conclusão da operação, invocamos o evento TarefaConcluida, passando a mensagem “Tarefa concluída”.

O método
TratarEventoAsyncpermanece inalterado e continua a ser um manipulador de eventos assíncronos.

No método Main, criamos uma instância de
ExemploAsync e também assinamos o evento TarefaConcluidausando uma expressão lambda. Quando o evento é gerado, a expressão lambda imprime a mensagem recebida no console.

4- Use os métodos Task.WhenAll ou Task.WhenAny para execução em paralelo

Os métodos Task.WhenAll e Task.WhenAny são métodos estáticos disponíveis em C# para trabalhar com tarefas assíncronas em paralelo, permitindo que você aguarde várias tarefas de forma eficiente.

  1. Task.WhenAll:
  1. Task.WhenAny:

Exemplo:

public class ExemploAsync
{
    public async Task<string> ObtemDadosAsync()
    {
        await Task.Delay(2000);
        return "Dados obtidos com sucesso.";
    }
    public async Task<string> ProcessaDadosAsync()
    {
        await Task.Delay(1500);
        return "Dados processados com sucesso.";
    }
    public async Task<string> SalvaDadosAsync()
    {
        await Task.Delay(1000);
        return "Dados salvos com sucesso.";
    }
    public async Task ProcessaDadosConcorrenteAsync()
    {
        //Usa Task<string> para armazenar resultados
        var tasks = new List<Task<string>>
        {
            ObtemDadosAsync(),
            ProcessaDadosAsync(),
            SalvaDadosAsync()
        };
        // Inicia todas as tafefas simultâneamente e aguarda que elas terminem
        string[] results = await Task.WhenAll(tasks);
        // Processa os resultados e faz outras operações
        foreach (string result in results)
        {
            Console.WriteLine(result);
        }
    }
    public static async Task Main(string[] args)
    {
        var example = new ExemploAsync();
        await example.ProcessaDadosConcorrenteAsync();
    }
}

Entendendo o código:

Neste exemplo, temos três métodos assíncronos: ObtemDadosAsync, ProcessaDadosAsync e SalvaDadosAsync onde cada método simula uma operação com um atraso específico.

O método ProcessaDadosConcorrenteAsync compõe esses métodos assíncronos em uma lista de tarefas, cada uma retornando um resultado de string. Usamos List<Task<string>> para armazenar as tarefas e seus resultados.

O método Task.WhenAll é usado para iniciar todas as tarefas simultaneamente e aguardar a conclusão de todas elas. Isso nos permite realizar múltiplas operações assíncronas simultaneamente, melhorando o desempenho geral.

Depois que todas as tarefas são concluídas, os resultados são coletados em uma série de strings, results. Você pode processar esses resultados ou realizar quaisquer outras operações necessárias. Neste exemplo, simplesmente imprimimos cada resultado no console.

Este código demonstra o poder de compor operações assíncronas usando Task.WhenAll. Ele permite que você execute várias tarefas assíncronas simultaneamente e espere com eficiência que todas elas sejam concluídas. Isso é especialmente útil em cenários onde você precisa executar diversas operações assíncronas em paralelo, como buscar dados de diferentes fontes, processá-los e salvá-los simultaneamente.

Esses são alguns cenários onde temos o uso da programação assíncrona e onde podemos aplicar estas boas práticas.

E estamos conversados...

"Finalmente apareceu (Jesus) aos onze, estando eles assentados à mesa, e lançou-lhes em rosto a sua incredulidade e dureza de coração, por não haverem crido nos que o tinham visto já ressuscitado."
Marcos 16:14

Referências:


José Carlos Macoratti