DEV Community

Angelo Belchior
Angelo Belchior

Posted on • Edited on

Async/Await: Task.Result e a morte dos Pandas de Madagascar

Desenho de um panda usando um notebook - Essa imagem foi gerada utilizando o Dall-E no Bing

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".

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;
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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:

Image description

ObterDadosAssincronos().ContinueWith(task =>
{
    if (task.Status == TaskStatus.RanToCompletion)
    {
        var result = task.Result; // Ok, não se assuste, eu explico mais abaixo :)
    }
}).Wait();
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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;  
}
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
renanosoriorosa profile image
Renan Osório

Material maravilhoso, meus parabéns.

Collapse
 
angelobelchior profile image
Angelo Belchior

Muito Obrigado

Collapse
 
haroldofv profile image
Haroldo Vinente

Excelente material, parabéns.