Introdução

Para escrever código C# assíncrono adequado e evitar deadlocks, você precisa entender alguns conceitos.

A configuração de boas práticas pode ajudar a evitar problemas comuns, mas às vezes isso não é suficiente, é quando você precisa entender o que está acontecendo.

Neste post vou abordar alguns destes conceitos. Vou considerar que você esteja familiarizado com o código assíncrono.

A primeira coisa que devemos entender é: Uma Task no C# não é uma Thread. A task é apenas um bloco de código de deve ser executado em algum momento, pense nela como uma Promisse do JavaScript.

Como funciona

Vamos para uma chamada simples no C#

public async Task<string> DownloadAsync()
{
    using (WebClient wc = new WebClient())
    {
        var page = await wc.DownloadStringTaskAsync("http://xpt.com");
        return page;
    }
}

Acima temos uma solicitação de forma assíncrona e o encadeamento fica livre para trabalhar em outras tarefas enquanto o servidor responde a requisição.
Ele agendará tarefas para execução, uma vez que uma tarefa seja concluída, outra tarefa será agendada.

Tudo o que você faz com assíncrono é esperar o término em uma fila de execução.
Cada tarefa é enfileirada usando TaskScheduler que pode fazer o que quiser com sua tarefa.

Uma observação importante, o TaskScheduler depende do contexto em que você está.

Vamos fazer algo agora que pode funcionar de forma intermitente, e você pode não perceber problemas a princípio, ou mesmo em produção, não obtendo a infeliz oportunidade de pegar o bug que o cliente sempre pega.

public string DownloadAsync()
{
     using (WebClient wc = new WebClient())
     {
         var page = wc.DownloadStringTaskAsync("http://xpt.com").Result;
         return page;
     }
}

Está ai, um exemplo que funciona, e pode gerar um deadlock no nível de aplicação.

Isso acontece porque a chamado o “.Result” irá que bloquear o encadeamento até que a tarefa seja concluída. Isso acabou com o objetivo de async, e fica bloqueado até a solicitação terminar.

Segundo exemplo inseguro

public string DownloadAsync()
{
     return Task.Run(async () =>
     {
         using (WebClient wc = new WebClient())
         {
             var page = await wc.DownloadStringTaskAsync("http://xpt.com");
             return page;
         }
     }).Result;
}

Está um exemplo que vejo com frequencia, infelizmente.

Pode parece muito simples este exemplo, porem pense em chamadas entre camadas da aplicação desta forma, ou seja, você não vai enxergar de forma tão visivel, como no exemplo, mas terá os mesmos efeitos colaterias.

O Task.Run força a execução acontecer no pool de threads.

Isso não seria um problema se você estivesse executando código stand alone, mas em um ambiente multi usuário, certamente irá enfrentar problemas com deadlock.

Enfileiramento das tarefas

SynchronizationContext este controla como suas tarefas são executadas.

Ele determinará o que você pode ou não fazer ao chamar funções assíncronas. Tudo o que as funções assíncronas fazem é agendar uma tarefa para o contexto atual.

O TaskScheduler pode agendar a execução da maneira que desejar.
Você pode implementar seu próprio TaskScheduler inclusive.
Você também pode implementar o seu próprio SyncronizationContext.
O SyncronizationContext é uma maneira genérica de enfileirar o trabalho para outras threads.

O TaskScheduler é apenas uma abstração que lida com o agendamento e a execução de tarefas.

Quando você cria uma tarefa, o C # usa TaskScheduler.Current para enfileirar a nova tarefa. Que por sua vez, usará o TaskScheduler da tarefa atual.

Em seguida deve verificar se há um contexto de sincronização associado ao encadeamento atual e o usa para agendar a execução de tarefas usando SynchronizationContext.Post, mas se não houver, usará o TaskScheduler.Default, que agendará a task em uma fila executada.

Diagrama para criação de nova task

Veja a imagem acima, de como uma nova task é enfileirada no contexto.

Cuidados que devemos tomar

Nos aplicativos de console, por padrão, você não possui um contexto de sincronização, mas possui uma thread principal.
As tarefas serão enfileiradas usando o TaskScheduler padrão e serão executadas no pool. Você pode bloquear a vontade sua thread principal, ele apenas para de executar.

Se você estiver em uma aplicação Web, o IIS alocará um para cada solicitação. Cada um desses encadeamentos possui seu próprio contexto de sincronização.
As tarefas são agendadas para esses threads por padrão. Se você chamar “.Result” em uma tarefa enfileirada, haverá um deadlock.

 

Por isso é muito importante você estar atento ao seu fluxo de execução, em chamadas async.

 

Espero que tenha sido útil.

Até a próxima!