Async/Await: Task.ConfigureAwait, Deadlock e Pink Floyd
Published: 2023-10-25 23:00:43

No [post anterior](https://dev.to/angelobelchior/por-debaixo-do-capo-asyncawait-e-as-magicas-do-compilador-csharp-28ol) eu falei um pouco sobre como o **compilador do csharp** resolve métodos assíncronos por debaixo do capô. Eu não quis entrar no conceito em si dessa _feature_, quis apenas focar em como as coisas *automágicamente* eram resolvidas.

---

**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"](https://www.sympla.com.br/evento-online/apis-robustas-com-asp-net-core-8-entity-framework-docker-e-redis/2315443?referrer=www.vemcodar.com.br) 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/](https://vemcodar.com.br/)**

---

Porém chegou a hora de falar um pouco sobre algumas características de programação assíncrona e paralelismo em geral.

Quando falamos sobre métodos assíncronos, várias dúvidas surgem. Neste post eu abordo uma das mais interessantes: A função do método `ConfigureAwait(true/false)` da classe `Task`.

Provavelmente você já deve ter visto algo como:

```csharp
var post = await blog.ObterPostPorIdAsync(1).ConfigureAwait(false);
```
Mas por algum momento você parou para pensar qual é a real necessidade disso? Quais são os impactos de se passar um `true` ou um `false` como argumento, ou ainda, o que acontece se eu omitir a chamada desse método?

Pra começar precisamos falar sobre `SynchronizationContext`!

## O que é o SynchronizationContext?

Em um processo assíncrono, o [SynchronizationContext](https://learn.microsoft.com/en-us/dotnet/api/system.threading.synchronizationcontext?view=net-7.0)  - _**contexto de sincronização**_ - refere-se ao ambiente no qual uma determinada tarefa (`Task` / `Thread`) é executada. Ele inclui informações sobre a **thread atual** como por exemplo a [cultura atual](https://learn.microsoft.com/pt-br/dotnet/api/system.threading.thread.currentculture?view=net-7.0) e a [cultura da interface de usuário](https://learn.microsoft.com/pt-br/dotnet/api/system.threading.thread.currentuiculture?view=net-7.0)  (caso seja um aplicativo desktop ou mobile), além de outros estados relacionados à execução da própria tarefa. 

Esse contexto é especialmente importante em cenários em que várias _tasks_ concorrem para acessar recursos compartilhados ou quando a interface de usuário precisa renderizar determinados resultados de tarefas, como por exemplo um processamento de um arquivo que a cada linha processada atualiza a porcentagem de uma barra de progresso na UI.  

Nesse caso dado como exemplo a _thread_ que gerencia a interface com o usuário (**UI Thread**) precisa estar disponível para gerenciar suas ações.  Um processo assíncrono não deveria travar a tela. Aliás, se você utiliza o Windows ja deve ter reparado que algumas vezes a janela do sistema fica toda branca (congelada) ou fazendo aquele efeito psicodélico digno de uma capa de um disco do **[Pink Floyd](https://www.youtube.com/watch?v=7kWl-ZGMwkQ&ab_channel=PinkFloyd)**. 

Isso geralmente ocorre porque a _**Thread de UI**_, responsável por renderizar a tela, está travada, provavelmente aguardando a finalização de algum processo que pode ou não ser assíncrono.

![Tela do Windows Travada](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ix7u83kdryg3kmxwouuo.jpeg)

> Acima a capa do disco raro do Pink Floyd chamado _The frozen side of the Windows_, lançado em 1995.

É importante entender em qual contexto de sincronização estamos executando uma `Task` porque influencia como uma tarefa assíncrona interage com seu ambiente. 

Quando uma tarefa assíncrona é criada, por padrão, ela captura o contexto de sincronização atual, no exemplo anterior o contexto era a _UI_. Isso significa que, ao ser concluída, a tarefa **tentará** retornar ao contexto original no qual foi iniciada. 

E como esse retorno ao contexto original é gerenciado no `ConfigureAwait`?

## Indo um pouco mais afundo: TaskScheduler

O `TaskScheduler` não é algo exclusivo da _feature_ _async/await_. Ele faz parte do modelo de programação baseado em tarefas ([TPL](https://learn.microsoft.com/en-us/dotnet/standard/parallel-programming/task-parallel-library-tpl)) fornecido pelo namespace `System.Threading.Tasks` e ajuda no gerenciamento de alocação de threads e na escalabilidade de operações assíncronas, o que é importante para evitar os _deadlocks_ (explico a seguir o que é isso) em aplicativos que dependem de I/O como leitura ou gravação de arquivos, solicitações de rede, operações de banco de dados, etc.

Um dos principais usos do `TaskScheduler` está relacionado ao método `ConfigureAwait`. Como disse acima, só que agora em outras palavras, esse método é usado para especificar em qual **contexto de sincronização a continuação de uma tarefa assíncrona deve ser executada após a tarefa principal ser concluída**. 

O `TaskScheduler` é usado para agendar essa continuação!

Por exemplo, ao passar `true` para o `ConfigureAwait`, o que é o default (você poderia omitir a chamada ao método nesse caso),  você está instruindo sua tarefa assíncrona a retornar ao contexto de sincronização original. 
Se passar `false`, **a continuação não terá a garantia de retornar ao contexto original, o que é útil quando você deseja evitar bloqueios (_deadlocks_), especialmente em operações de I/O demoradas**.

## Evitando Deadlocks entre Threads

Saber como o `ConfigureAwait` funciona pode (e vai) evitar problemas de [**deadlocks**](https://learn.microsoft.com/en-us/archive/msdn-magazine/2006/april/avoiding-and-detecting-deadlocks-in-net-apps-with-csharp-and-c) entre threads.  

Um **deadlock** (também conhecido como **pandemônio**) ocorre quando duas ou mais **threads** aguardam indefinidamente por recursos que estão sendo mantidos (travados) por outras threads, resultando em uma situação de bloqueio mútuo. Isso ocorre quando as threads bloqueiam recursos e esperam por recursos que estão bloqueados por outras threads, impedindo que qualquer uma delas prossiga. 

> Esse conceito é muito comum em banco de dados. No MSSQL por exemplo, é muito recomendado usar o `(NOLOCK)` ao fazer consultas: `Select Id, Nome From Pessoas (NOLOCK)`. Mais informações, acesse essa [documentação](https://learn.microsoft.com/pt-br/sql/relational-databases/sql-server-deadlocks-guide?view=sql-server-ver16).

Abaixo segue um programa onde causamos propositalmente um **deadlock**:

```csharp
public static class Program
{
    public static async Task Main()
    {
        var recursoA = new Recurso();
        var recursoB = new Recurso();

        var task1 = Task.Run(() =>
        {
            Console.WriteLine("Task 1: Dando lock no recurso A para que seja possível manipulá-lo");
            lock (recursoA)
            {
                recursoA.SomaUm();
                Console.WriteLine("Task 1: Lock no recurso A efetuado. Agora tentando dar lock no recurso B");
                lock (recursoB)
                {
                    Console.WriteLine("Task 2: Recursos A e B estão locados, nunca vai chegar aqui");
                    recursoB.SubtraiUm();
                }
            }
        });

        var task2 = Task.Run(() =>
        {
            Console.WriteLine("Task 2: Dando lock no recurso B para que seja possível manipulá-lo");
            lock (recursoB)
            {
                recursoB.SomaUm();
                Console.WriteLine("Task 2: Lock no recurso B efetuado. Agora tentando dar lock no recurso A");
                lock (recursoA)
                {
                    Console.WriteLine("Task 2: Recursos B e A estão locados, nunca vai chegar aqui");
                    recursoA.SubtraiUm();
                }
            }
        });

        await task1;
        await task2;
        
        // Daqui pra baixo nada acontecerá, pois as tasks nunca vão terminar
        
        Console.WriteLine("Se tudo der certo e nada der errado, essa mensagem nunca será exibida");
    }
}

public class Recurso
{
    private int _total = 0;
    public int Total => _total;
    public void SomaUm()
    {
        _total++;
        Thread.Sleep(120); // simulação de um processo externo demorado
    }
    
    public void SubtraiUm()
    {
        _total--;
        Thread.Sleep(120); // simulação de um processo externo demorado
    }
}
```

A saída de execução desse programa é:

```bash
Task 1: Dando lock no recurso A para que seja possível manipulá-lo
Task 2: Dando lock no recurso B para que seja possível manipulá-lo
Task 1: Lock no recurso A efetuado. Agora tentando dar lock no recurso B
Task 2: Lock no recurso B efetuado. Agora tentando dar lock no recurso A
```

Você pode obter o código fonte desse exemplo clicando [aqui](https://github.com/angelobelchior/DeadLockMalefico).

**Explicando!**

Duas variáveis são criadas, `recursoA` e `recursoB`. Essas variáveis serão usadas para criar _locks_ e simular a contenção de recursos compartilhados.

Em seguida são criadas duas `Tasks`, `task1` e `task2`, onde é executado um método anônimo onde cada uma dessas funções tenta adquirir locks nos recursos compartilhados, mas em ordens diferentes. 
Isso cria uma situação em que a `task1` bloqueia `recursoA` e aguarda `recursoB`, enquanto a `task2` bloqueia `recursoB` e aguarda `recursoA`.

Que bagunça! 

Como resultado, ambas as `Tasks` ficam presas, aguardando que a outra libere o recurso que elas precisam para continuar, causando um **deadlock**.

O resultado da execução desse código será que as mensagens de bloqueio e espera serão impressas, mas o programa nunca chegará à mensagem **"Se tudo der certo e nada der errado, essa mensagem nunca será exibida"** porque as duas _tasks_ estarão presas em um **deadlock**.

Esse tipo de situação pode resultar em um programa que fica congelado e não pode continuar a execução. Lembram do que citei acima sobre telas em branco ou efeitos psicodélicos? Então... 

> É claro que esse exemplo é um caso extremo, e nem sequer adiantaria utilizar o `ConfigureAwait(false)` já que o contexto de sincronização está sendo executado pelo `lock` (mais a frente eu explico melhor o que seria isso);

É muito complexo criar uma simulação real desse tipo de cenário, por isso, para fins didáticos, optei por usar esse exemplo. Mas acredite, no mundo real é diferente! Quando temos uma aplicação grande, acessada por milhares e milhares de pessoas, caso não seja tomado os devidos cuidados, podemos cair num cenário de **deadlock**. Lembrem-se de que **as ruas não são como a Disneylândia**! Produção é mundo real!

É por isso que precisamos ter algo que "orquestre" a interação entre as threads para que esse tipo de coisa não ocorra.

**Deadlock** é algo tão ruim, mas tão ruim, que consegue sair do mundo virtual e ferrar nossas vidas no mundo real. Não acredita?

Saca só:

![Imagem retirada do site https://exame.com/brasil/falha-em-semaforo-causa-engarrafamento-homerico-em-sp/](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kh8zqxzlkbtm65ua7eec.jpg)

> [Uma falha no semáforo entre as avenidas Faria Lima e Juscelino Kubitschek travou a movimentação de carros. CET levou 20 minutos para resolver problema](https://exame.com/brasil/falha-em-semaforo-causa-engarrafamento-homerico-em-sp/) - Revista Exame

Na imagem temos um cruzamento onde todas as saídas estão bloqueadas por carros. 

*E tem empresa querendo voltar ao trabalho presencial... enfim... segue o velório...*

Imagine que cada carro da imagem acima é uma _thread_. 

Imaginou? Então... acho que agora ficou claro, certo?

Para evitar **deadlocks**, é importante coordenar o acesso a recursos compartilhados usando mecanismos de sincronização (`SynchronizationContext`). Na foto acima esse mecanismo seria um **semáforo** (**farol** pra quem é de São Paulo) ou um guarda da CET. Isso garantiria que as _threads_ liberariam recursos quando terminarem de usá-los.

Ainda temos a possibilidade de usarmos [locks](https://learn.microsoft.com/pt-br/dotnet/csharp/language-reference/statements/lock), [**semáforos**](https://learn.microsoft.com/pt-br/dotnet/api/system.threading.semaphore?view=net-7.0) ou [mutex](https://learn.microsoft.com/pt-br/dotnet/api/system.threading.mutex?view=net-7.0), mas não quero expandir mais esse post, já que cada um desses itens de sincronização mereceria uma atenção especial. Dediquem tempo para lerem a documentação de cada um deles, lá tem tudo que vocês precisam para entender como as coisas funcionam.

Destaco aqui que, mesmo usando as técnicas que eu citei acima, não é garantido que seu código vai rodar sem nenhum **deadlock**. A prova disso foi o exemplo de código dado anteriormente.

> **O mais importante aqui é entendermos que é possível informar a `Task` que não é necessário se preocupar em voltar ao contexto inicial - `ConfigureAwait(false)` - e assim evitarmos de cair em um _deadlock_ maléfico.**

## Cenários interessantes e outras abordagens

### Aplicações Desktop e Mobile

Como disse acima, se o contexto capturado for bloqueado por outra operação que aguarda a conclusão de uma tarefa, ocorrerá um **deadlock**. 

Aplicações com interface visual (UI) são os exemplos mais interessantes para explorarmos já que se a thread da UI **esperar de forma síncrona** a conclusão de uma tarefa ou ocorrer um **deadlock**, ela não vai conseguir renderizar os controles de tela, por exemplo.

Porém, quando falamos em aplicações desse tipo, temos implementações específicas que cuidam dessa gestão de sincronismo para nós. **Em aplicativos com interface de usuário, é crucial garantir que as operações assíncronas não bloqueiem a Thread de UI**. Uma aplicação fluída é aquela que é possível utilizar sem que haja nenhum engasgo no processamento ou na renderização dos componentes visuais.  

Em [Windows Forms](https://learn.microsoft.com/pt-br/dotnet/desktop/winforms/overview/?view=netdesktop-7.0), [WPF](https://learn.microsoft.com/pt-br/dotnet/desktop/wpf/overview/?view=netdesktop-7.0), [UWP](https://learn.microsoft.com/pt-br/windows/uwp/get-started/universal-application-platform-guide), [WinUI](https://learn.microsoft.com/en-us/windows/apps/winui/), [Xamarin Forms](https://learn.microsoft.com/pt-br/xamarin/xamarin-forms/) e [MAUI](https://learn.microsoft.com/pt-br/dotnet/maui/what-is-maui) temos o conceito do *Dispatcher* .

O `Dispatcher` é responsável por gerenciar a thread principal da interface de usuário (**Thread de UI**). Ele fornece um mecanismo para executar operações nessa thread de forma segura e garantindo que não ocorra **deadlocks**. 

Basicamente, as plataformas citadas acima implementam seu próprio modelo de `Dispatcher`, porém todos tem a mesma característica: um método `Invoke` e um método `BeginInvoke` (pode ser que os nomes não sejam esses, mas a ideia é a mesma).

O método `Invoke` executa de forma síncrona o código na thread de UI e aguarda seu término.
Já o método `BeginInvoke` coloca o código na fila para ser executado na thread de UI, de forma assíncrona (lembram do `TaskScheduler`? Ele é quem vai garantir que o processo retorne ao contexto correto assim que for tirado da fila e executado).

No final, **cada plataforma vai ter a sua própria implementação do `SynchronizationContext`**, mas ideia por debaixo do capô é sempre a mesma: Evitar que ocorra **deadlocks**.

### ASP.NET Core (Web e API) 

Curiosamente nesse cenário não existe uma implementação do `SynchronizationContext`!
[Stephen Cleary](https://blog.stephencleary.com/) vai mais a fundo nesse assunto neste [post](https://blog.stephencleary.com/2017/03/aspnetcore-synchronization-context.html)!

Ele diz que um dos principais motivos para não existir essa abordagem é a performance:

> Quando um mecanismo assíncrono retoma a execução no ASP.NET legado (WebForms, etc.), a continuação é enfileirada no contexto da solicitação. A continuação deve aguardar por quaisquer outras continuações que já tenham sido enfileiradas (apenas uma pode ser executada de cada vez). Quando estiver pronto para ser executado, uma thread é retirada do pool de threads, entra no contexto da solicitação e, em seguida, retoma a execução do manipulador. Esse "reingresso" no contexto da solicitação envolve várias tarefas de manutenção, como definir _HttpContext.Current_ e a identidade e cultura da thread atual.

Com o sincronismo de contexto, se faz necessário redefinir as informações do `HttpContext.Current`, e ai temos um ponto crítico: Isso leva um bom tempo.

Essas redefinições são necessárias porque o contexto pode ter sido alterado em algum momento. Por isso existe essa gestão de sincronização de _tasks_. **Não existe almoço grátis!**

Com o avanço dessa plataforma, o time do ASP.NET Core focou em performance. Nem preciso dizer o quão absurdamente performáticas as últimas versões estão (no momento em que escrevo estamos a poucas semanas do lançamento final do .Net 8). 

Você mesmo pode avaliar todo o resultado dessa obsessão por performance lendo os posts que o time disponibiliza. Eu separei o mais atual: [Performance Improvements in ASP.NET Core 8 - .NET Blog (microsoft.com)](https://devblogs.microsoft.com/dotnet/performance-improvements-in-aspnet-core-8/).

Certo, se não existe `SyncronizationContext`, como que os métodos assíncronos funcionam? 

Simples: Códigos que estão sendo executados no "ambiente" ASP.NET Core não precisam mais configurar qual contexto uma Task deve retornar após sua finalização. Isso é feito internamente. Logo, para nós não é exposta a possibilidade de alterar essa configuração. O time de desenvolvimento resolveu isso sincronizando *automágicamente* por debaixo do capô. 

Isso é algo que eu ainda preciso entender mais a fundo, porém só lendo o código fonte do ASP.NET Core mesmo para ter mais noção. Quem sabe eu crie um post falando sobre, e se você sabe como funciona, deixe nos comentários :)

Porém, isso não quer dizer que você, pessoa desenvolvedora _backend_ não deva se preocupar com isso. Muito pelo contrário! Em um "ambiente" ASP.NET Core isso está resolvido, mas quando criamos/utilizamos bibliotecas (de terceiros ou não), precisamos ter total noção de como as coisas funcionam para saber se devemos ou não utilizar o `ConfigureAwait(false)`. 

Nesses cenários ainda corremos sérios riscos de **deadlock**. 

### ASP.NET Core Blazor (Server e WebAssembly)

Saca só que interessante: O Blazor utiliza seu próprio contexto de sincronização para garantir uma única _thread_ lógica de execução. Os métodos do ciclo de vida de um componente e os _callbacks_ de eventos gerados pelo Blazor são executados nesse contexto.

O contexto de sincronização no lado do servidor tenta emular um ambiente _single-thread_, assim como é feito no lado do navegador (WebAssembly), que trabalha com uma thread única. Qualquer processo é forçado a ser realizado nessa única thread. Não existe processamentos simultâneos e por isso devemos evitar usar métodos/propriedades bloqueantes como o `.Result`, `.Wait()` ou o `GetResult()` da classe `Task`.

Mais informações (que aliás são bem escassas sobre esse assunto no Blazor) aqui: [Contexto de sincronização do Blazor ASP.NET Core | Microsoft Learn](https://learn.microsoft.com/pt-br/aspnet/core/blazor/components/synchronization-context?view=aspnetcore-7.0)

---

> Eu gostaria que com esse post, você meu caro leitor, minha cara leitora, entenda que conhecer como o `ConfigureAwait` funciona é crucial para otimizar o desempenho e evitar bloqueios desnecessários em aplicativos assíncronos

Essa é a mensagem que eu gostaria que ficasse na sua cabeça! Espero que tenham entendido a importância de conhecer bem como funciona o `ConfigureAwait`, porém o  melhor post do universo que trata sobre esse assunto é esse: 
[ConfigureAwait FAQ - .NET Blog (microsoft.com)](https://devblogs.microsoft.com/dotnet/configureawait-faq/). 

Leitura mais do que obrigatória!

Era isso. Espero que tenham gostado e até a próxima!