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:
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(); } |
Este código se enquadra no anti-padrão por dois motivos :
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.
public
async Task<string>
GetDadosPrevisao(string
cidade) { //.... código } |
private
async void
btnPrevisao_Click(object
sender, EventArgs e) { txtResultado.Text = await GetDadosPrevisao(txtCidade.Text); } |
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); }
return await httpResponse.Content.ReadAsStringAsync(); |
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:
ASP.NET Core Web API - Apresentando API Analyzers
ASP.NET Core - Usando o token JWT com o Swagger