Continuando a discussão sobre "Métodos Assíncronos e Deadlocks" que eu comecei no post anterior, neste artigo abordarei algumas das situações mais trágicas que acontecem quando o uso do async/await
é empregado incorretamente, focando especificamente na problemática conhecida como "A morte dos Pandas de Madagascar".
Antes de continuar, #VemCodar com a gente!!
Tá afim de criar APIs robustas com .NET?
Feito de dev para dev, direto das trincheiras, por quem coloca software em produção há mais de 20 anos, o curso "APIs robustas com ASP.NET Core 8, Entity Framework, Docker e Redis" traz de maneira direta, sem enrolação, tudo que você precisa saber para construir suas APIs de tal forma que elas sejam seguras, performáticas, escaláveis e, acima de tudo, dentro de um ambiente de desenvolvimento totalmente configurado utilizando Docker e Docker Compose.
Não fique de fora! Dê um Up na sua carreira!
O treinamento acontecerá nos dias 27/02, 28/02 e 29/02, online, das 19.30hs às 22.30hs
Acesse: https://vemcodar.com.br/
Numa recente pesquisa feita pelo NID2A - Nogare Institute for Data Analysis and Astrology - cada vez que é usada a propriedade
.Result
da forma errada em um sistema .Net, morrem ao menos 3 Pandas.
O uso de async/await
tornou-se uma prática comum no desenvolvimento de software moderno, permitindo que sejam criadas aplicações mais responsivas e eficientes, porém, ainda existem pessoas que não entenderam bem o tamanho do problema quando utilizamos de forma errada a propriedade .Result
da classe Task
.
No post anterior eu falei bastante sobre deadlocks (acho que eu citei ao menos 20 vezes essa palavra lá) e agora eu quero continuar esse assunto, porém, explorando um pouco mais a fundo o .Result
.
Mas afinal, o que o .Result faz?
O .Result
é uma maneira de obter o resultado de uma operação assíncrona em C#:
var post = blog.ObterPostPorIdAsync(1).Result;
Quando uma tarefa assíncrona é chamada usando await
, a thread original não é bloqueada, permitindo que outras tarefas sejam executadas.
No entanto, usar .Result
em uma tarefa assíncrona bloqueará a thread original até que a tarefa seja concluída. E você já deve imaginar o que pode dar errado nessa situação, certo? Não?? Ok, eu explico!
Quando você utiliza o .Result
em aplicativos com várias threads, com grandes quantidades de operações assíncronas, isso pode levar a um uso ineficiente dos recursos do sistema, diminuindo o desempenho e a capacidade de resposta do aplicativo.
E o pior, caso o .Result
seja usado em um fluxo onde também temos o uso do .Wait()
é muito provável que ocorra um deadlock. Se a operação assíncrona exigir acesso a um recurso que está sendo bloqueado por outra parte do código, o .Result
pode criar em um impasse, onde ambas as threads ficam esperando indefinidamente uma pela outra, prejudicando o funcionamento da aplicação.
Fora que o uso do .Result
também anula um dos principais benefícios do async/await
: a concorrência! A ideia por trás do async/await
é liberar a thread original para que ela possa realizar outras tarefas enquanto a operação assíncrona é executada. Se essa thread está travada, perde-se a capacidade de aproveitar ao máximo esse benefício.
Alternativas ao uso do .Result
Agora que você entendeu que podemos ter sérios problemas ao usar o .Result
, abaixo eu cito algumas alternativas. Mas reforço novamente que é preciso ter um entendimento amplo de como as coisas funcionam por debaixo do capô quando falamos de métodos assíncronos.
Eu tenho alguns posts que tratam desse assunto.
Vamos começar!
A primeira e principal recomendação é o uso do await
para aguardar a conclusão de uma operação assíncrona.
var resultado = await ObterDadosAssincronos();
Essa deve ser sempre a primeira opção. Tente organizar seu código para seja possível utilizar essa abordagem!
O uso do .GetAwaiter().GetResult()
é mais seguro do que .Result
pois esse método permite aguardar o resultado da tarefa sem bloquear a thread orignal. Dos mares, o mais calmo, dos males, o menor.
var resultado = ObterDadosAssincronos().GetAwaiter().GetResult();
Isso permite que a thread que invocou esse método permaneça ativa e responsiva, sem nenhum bloqueio.
Se você estiver em um contexto em que o uso de await
não é possível (por exemplo, em um método síncrono), você pode usar Task.Wait
ou Task.WaitAll
para aguardar a conclusão da task. No entanto, lembre-se de que, caso exista alguma parte do código que use um lock
ou um .Result
a thread original pode ficar travada e isso pode causar problemas de desempenho e o/ou o famigerado deadlock
.
var task1 = ObterDadosAssincronos().Wait();
//
var taskA = ProcessamentoAssincronosA();
var taskB = ProcessamentoAssincronosB();
var taskC = ProcessamentoAssincronosC();
Task.WaitAll(taskA, taskB, taskC);
Outra abordagem é usar callbacks com o método Task.ContinueWith
para tratar o resultado de tarefas assíncronas quando elas forem concluídas. Isso evita bloqueios na thread principal. Porém isso me lembra javascript e, consequentemente, aquela imagem do Ryu me vem à mente:
ObterDadosAssincronos().ContinueWith(task =>
{
if (task.Status == TaskStatus.RanToCompletion)
{
var result = task.Result; // Ok, não se assuste, eu explico mais abaixo :)
}
}).Wait();
Eu não gosto dessa abordagem. Prefiro organizar o código para utilizar o await
, porém essa é uma alternativa válida. Mas tenha muito cuidado com o callback hell!
"Mas Angelo, o construtor de uma classe no csharp não permite usar async/await
, por isso, só nesse cenário que eu uso o .Result
. Eu juro!!!!"
Seria algo como:
public class ParserDeArquivo
{
public string Conteudo { get; }
public ParserDeArquivo(string caminho)
{
using var stream = new FileStream(caminho, FileMode.Open);
using var reader = new StreamReader(stream);
Conteudo = reader.ReadToEndAsync().Result;
}
}
var parser = new ParserDeArquivo("arquivo.txt");
Ok, não vou dedicar tempo para pensar no motivo pelo qual precisamos invocar um método assíncrono no construtor, mas eu sugiro uma forma de se fazer isso que, além de ser elegante, não mata nenhum Panda!
public class ParserDeArquivo
{
public string Conteudo { get; }
private ParserDeArquivo(string conteudo)
=> Conteudo = conteudo;
public static async Task<ParserDeArquivo> CriarAsync(string caminho)
{
using var stream = new FileStream(caminho, FileMode.Open);
using var reader = new StreamReader(stream);
var conteudo = await reader.ReadToEndAsync();
return new ParserDeArquivo(conteudo);
}
}
var parser = await ParserDeArquivo.CriarAsync("arquivo.txt");
Explicando: Basicamente, é criado um construtor privado, dessa forma não é possível que essa classe seja instanciada por ninguém, a não ser por métodos dentro dela própria.
No lugar do construtor, temos um método estático assíncrono chamado CriarAsync
onde passamos o caminho do arquivo por parâmetro. Esse método cria a instância da classe através do construtor privado passando o conteúdo do arquivo que foi lido de forma assíncrona como parâmetro.
Viu só? Nem doeu!
Mas então para que serve o .Result?
Você provavelmente deve ter se perguntado: Se é tão ruim assim, por qual motivo o .Result
foi criado?
Se a gente ler a documentação da Microsoft, podemos notar que no exemplo apresentado, o .Result
é utilizado quando temos várias tarefas que precisam ser disparadas ao mesmo tempo pelo método Task.WaitAll(tasks.ToArray());
. Esse método aguarda a lista de tasks passadas por parâmetro finalizarem. Feito isso necessitamos usar o .Result
para obter o valor processado por cada task;
Segue um exemplo:
var task1 = CalcularValor(1);
var task2 = CalcularValor(2);
var task3 = CalcularValor(3);
await Task.WhenAll(task1, task2, task3);
Console.WriteLine($"Valor da tarefa 1: {task1.Result}");
Console.WriteLine($"Valor da tarefa 2: {task2.Result}");
Console.WriteLine($"Valor da tarefa 3: {task3.Result}");
return;
static async Task<int> CalcularValor(int numero)
{
await Task.Delay(1000);
return numero * 10;
}
Nesse contexto, só vamos acessar a propriedade .Result
se, e somente se, as taks estiverem finalizadas. Nesse momento não existe mais concorrência ou paralelismo (no contexto dessa task) e por isso é seguro invocar essa propriedade.
Clique aqui para acessar o código fonte do exemplo!
Outro caso onde precisamos utilizar o .Result
foi mostrado acima com o método ContinueWith
! Porém nesse caso é necessário validar o status de execução da task. No exemplo, só vamos obter o valor do .Result
caso o status da task seja RanToCompletion
, que significa que "A tarefa concluiu a execução com sucesso"!
Se você utiliza alguma outra abordagem para tratar de cenários onde é necessário invocar um método assíncrono dentro de um construtor, deixe aí nos comentários :)
Agora aquela dica marota!
David Fowler, que é Distinguished Engineer na Microsoft no time do ASP.NET Core e criador do SignalR criou um repositório no Github com boas práticas para o uso do async/await.
Para acessar clique aqui.
Essa documentação é obrigatória. Esse é o tipo de link que você deveria ter salvo nos favoritos!
E chegamos ao final do post. Esse assunto rende bastante e queri trazer mais conteúdos sobre ele no futuro.
Espero que tenham curtido.
Até a próxima!
Top comments (3)
Material maravilhoso, meus parabéns.
Muito Obrigado
Excelente material, parabéns.