C#  -  Tratando com métodos assíncronos corretamente


 Neste artigo vamos recordar como tratar de forma correta métodos assíncronos na linguagem C#.

Caso você não saiba, na linguagem C#, você deve sempre usar as palavras-chaves async/await quando estiver tratando com tarefas assíncronas ou Tasks.

A palavra-chave async transforma um método em um método assíncrono, que permite usar a palavra-chave await. Quando a palavra-chave await é aplicada, ela suspende o método de chamada e devolve o controle ao chamador até que a tarefa awaited seja concluída. Dessa forma, await só pode ser usado dentro de um método assíncrono.

Se você estiver usando ".GetAwaiter().GetResult()", ".Result" ou ".Wait()" para obter o resultado de uma tarefa ou aguardar a conclusão da tarefa assíncrona, você poderá enfrentar deadlocks ou inanição do pool de threads.

Ocorre que às vezes, ".GetAwaiter().GetResult()" é apresentado como uma boa maneira de substituir ".Result" e ".Wait()" quando não podemos usar async/await. Essa ideia pode ser perigosa.

Embora usar estes métodos seja uma solução muito melhor do que usar ".Result" e ".Wait()" devido ao tratamento de erros ser muito melhor, e o rastreamento de pilha de uma determinada expressão ser muito mais limpo,  verdade, ao usar ".GetAwaiter()" você está contando com ".Wait()", então você pode experimentar os deadlocks ou a inanição do pool de threads.

Assim, a recomendação é evitar a todo custo usar ".GetAwaiter().GetResult()", ".Result" ou ".Wait()", e,  para fazer isso tente refatorar seu código de cima para baixo para usar async/await.

O que podemos fazer se não pudermos usar Async/Await ?

Existem cenários onde temos que invocar um método assíncrono a partir de um método síncrono uma situação também conhecida como 'Sync over Async'.

O anti-padrão Sync over Async ocorre quando você está usando uma espera de bloqueio, ou seja, um Wait(), em um método async (assíncrono) , em vez de aguardar os resultados de forma assíncrona (usar await). Isso desperdiça o encadeamento, causa falta de resposta (se chamado da interface do usuário) e expõe você a possíveis deadlocks.

Existem duas causas para esta ocorrência:

  1. Invocar o método Wait() na tarefa(Task) retornada pela chamada assíncrona(async);
  2. Usar a propriedade Task.Result o que na verdade causa uma espera (wait) de bloqueio;

Nota:  Usar task.Result é acessar o get da propriedade que bloqueia a thread de chamada até que a operação assíncrona seja concluída; é equivalente a chamar o método Wait. Uma vez que o resultado de uma operação esteja disponível, ele é armazenado e retornado imediatamente nas chamadas subsequentes para a propriedade Result.

Exemplo do anti-padrão 'Sync over Async'

Para mostrar o problema do anti-padrão 'Sync over Async' vou criar uma aplicação Windows Forms chamada WF_Async  no .NET 6.0, que vai acessar a API OpenWeather para obter os dados da previsão de tempo para uma determinada cidade que será informada no formulário.

Nota: Para usar a API você terá que criar uma conta e cria a sua API_KEY no site: https://home.openweathermap.org/

Abaixo temos a aplicação exibindo o resultado da execução para a cidade de São Paulo:

Abaixo temos o código principal que mostra as duas causas do anti-padrão :

Código do evento Click do botão - Acessar Previsão :

private void btnPrevisao_Click(object sender, EventArgs e)
{
        txtResultado.Text = GetDadosPrevisao(txtCidade.Text);
}

Código do método GetDadosPrevisao(string cidade) que acessa a API e retorna a previsão :


public string GetDadosPrevisao(string cidade)
{
  var url =
$"http://api.openweathermap.org/data/2.5/weather?q={cidade}
                           &units=imperial&APPID={API_KEY}
";
  try
  {
      var responseTask = httpClient.
GetAsync(url);
    
 responseTask.Wait();
      responseTask.Result.EnsureSuccessStatusCode();

      var contentTask = responseTask.Result.Content.ReadAsStringAsync();
      string responseData =
contentTask.Result;
      return responseData;

   }
   catch (Exception ex)
   {
      MessageBox.Show(ex.Message);
      throw;
    }
 }

Este código se enquadra no anti-padrão por dois motivos :

  1.  Está chamando Wait() na tarefa retornada por GetAsync() ;
  2.  Esta usando .Result da tarefa retornada por ReadAsStringAsync();

Vejamos a seguir como corrigir o código.

Convertendo o método GetDadosPrevisao() para async

Para corrigir o código, precisaremos aguardar as Tasks retornadas pelos métodos assíncronos. Antes de podermos fazer isso, precisaremos converter o método para assíncrono.

Agora que o método GetDadosPrevisao() é um método assíncrono podemos usar await em GetAsync()

Vamos adicionar a palavra-chave await antes de GetAsync() e dessa forma não estamos mais recebendo de volta uma Tarefa, mas o resultado da Tarefa – um objeto HttpResponse. Então, vamos renomear responseTask para httpResponse.

E no código como estamos usando o .Result na tarefa retornada por ReadAsStringAsync() isso causa uma espera de bloqueio e esse erro é mais fácil de cometer, porque não é óbvio que chamar .Result resultaria em uma espera de bloqueio.

Para corrigir vamos adicionar a palavra-chave await antes de ReadAsStringAsync() e retornar o resultado.

Assim o código agora deve ficar assim:

private async void btnPrevisao_Click(object sender, EventArgs e)
{
        txtResultado.Text = await GetDadosPrevisao(txtCidade.Text);
}


public async Task<string> GetDadosPrevisao(string cidade)
{
   var url = $"http://api.openweathermap.org/data/2.5/weather
             ?q={cidade}&units=imperial&APPID={API_KEY}";
   try
   {
      var httpResponse = await httpClient.GetAsync(url);
      httpResponse.EnsureSuccessStatusCode();

      return await httpResponse.Content.ReadAsStringAsync();
    }
    catch (Exception ex)
    {
       MessageBox.Show(ex.Message);
       throw;
    }
}

E estamos conversados...

'Como, pois, invocarão aquele em quem não creram? e como crerão naquele de quem não ouviram? e como ouvirão, se não há quem pregue?'
Romanos 10:14

Referências:


José Carlos Macoratti