Deadlocks assíncronos são um dos bugs mais confusos em C#. A aplicação trava. Sem exceção. Sem erro. A requisição simplesmente nunca responde. Entender por que eles acontecem — e, mais importante, por que eles não acontecem em determinados contextos — é conhecimento essencial para todo desenvolvedor .NET.
O Cenário do Deadlock
Este código gera um deadlock no ASP.NET clássico e em aplicações de interface gráfica (WinForms, WPF):
// Em um controller ASP.NET clássico ou handler de evento WinForms
public ActionResult GetData()
{
// .Result bloqueia o thread atual e aguarda a conclusão da task
var data = FetchDataAsync().Result;
return View(data);
}
private async Task<string> FetchDataAsync()
{
// Este await captura o SynchronizationContext atual
var result = await httpClient.GetStringAsync("https://api.example.com/data");
return result;
}A aplicação trava indefinidamente. Aqui está exatamente o motivo.
Este não é um cenário teórico: o travamento é reproduzido de forma determinística em
samples/async-deadlocks-csharp
— uma aplicação de console que instala um SynchronizationContext estilo UI de ~40
linhas (a mesma regra de um-único-thread que WinForms e WPF impõem), provoca o deadlock,
o observa com um timeout em vez de travar, e depois verifica com asserções que cada rota
de fuga deste artigo realmente escapa.
Por Que o Deadlock Acontece
O deadlock requer que duas condições ocorram simultaneamente:
- Existe um
SynchronizationContextno thread que faz a chamada - O thread que faz a chamada está bloqueado com
.Resultou.Wait()
A sequência:
GetData()executa no thread de requisição do ASP.NET (que possui umSynchronizationContext)FetchDataAsync()inicia e chega aoawait httpClient.GetStringAsync(...)awaitcaptura oSynchronizationContextatual — ele registra "quando isso for concluído, retomar no contexto de requisição do ASP.NET"- O controle retorna para
GetData(), que chama.Result— isso bloqueia o thread de requisição do ASP.NET - A chamada HTTP é concluída e tenta retomar
FetchDataAsync()no contexto de requisição do ASP.NET - Mas o contexto de requisição está ocupado pela chamada
.Resultbloqueada - Deadlock. A task aguarda o thread; o thread aguarda a task.
Thread (bloqueado em .Result)
└── aguardando FetchDataAsync ser concluída
└── aguardando retomar NESTE thread
└── mas este thread está bloqueado
└── DEADLOCKO sample prova que essa espera mútua é o que realmente acontece, não apenas uma história
plausível: quando o Wait com timeout finalmente libera o thread bloqueado, a
continuação enfileirada executa e a task se completa sozinha, imediatamente. A task
nunca esteve presa — só esperava pelo thread que esperava por ela.
Por Que o ASP.NET Core Não Gera Deadlock
O ASP.NET Core não possui SynchronizationContext. Esta é uma decisão de design deliberada. Quando await é concluído no ASP.NET Core, ele retoma em qualquer thread disponível do thread pool — não tenta voltar a um thread específico.
// Isso não gera deadlock no ASP.NET Core (mas ainda é uma má prática!)
[HttpGet]
public IActionResult GetData()
{
var data = FetchDataAsync().Result; // Sem deadlock no Core — mas ainda desperdiça um thread
return Ok(data);
}Mesmo que .Result não gere deadlock no ASP.NET Core, ele ainda bloqueia um thread do thread pool enquanto aguarda. Sob carga, isso esgota o thread pool. Nunca use .Result ou .Wait() em handlers de requisição do ASP.NET Core.
O Problema com SynchronizationContext
SynchronizationContext é uma abstração que controla onde o código é executado após a conclusão de uma operação assíncrona. Frameworks diferentes têm contextos diferentes:
| Contexto | Onde o código retoma após await |
|---|---|
| ASP.NET Clássico | Thread de requisição original |
| WinForms | Thread de UI |
| WPF | Thread de UI (Dispatcher) |
| ASP.NET Core | Qualquer thread do thread pool (sem SynchronizationContext) |
| Aplicação console | Qualquer thread do thread pool (sem SynchronizationContext) |
| Testes xUnit | Contexto personalizado (pode gerar deadlock!) |
ConfigureAwait(false) como Solução
ConfigureAwait(false) diz ao await para não capturar o SynchronizationContext:
private async Task<string> FetchDataAsync()
{
// ConfigureAwait(false) — retomar em qualquer thread do thread pool
var result = await httpClient.GetStringAsync("https://api.example.com/data")
.ConfigureAwait(false);
return result;
}Agora a cadeia de deadlock está quebrada. Quando a chamada HTTP é concluída, ela retoma em um thread do thread pool (não no contexto de requisição capturado), então a chamada .Result bloqueada não está mais no caminho.
ConfigureAwait(false) em Código de Biblioteca
O código de biblioteca deve sempre usar ConfigureAwait(false). Bibliotecas não controlam o ambiente de chamada — sua biblioteca pode ser chamada a partir de WinForms, ASP.NET clássico, ou de qualquer lugar com um SynchronizationContext.
// Bom código de biblioteca — seguro para chamar de qualquer lugar
public static async Task<ApiResponse> GetApiDataAsync(string url)
{
var response = await httpClient.GetAsync(url).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
return JsonSerializer.Deserialize<ApiResponse>(content)!;
}A Solução Real: Async do Começo ao Fim
ConfigureAwait(false) é um paliativo. A solução real é nunca bloquear em código assíncrono:
// INCORRETO
public ActionResult GetData()
{
var data = FetchDataAsync().Result; // Bloqueante
return View(data);
}
// CORRETO — async do começo ao fim
public async Task<ActionResult> GetDataAsync()
{
var data = await FetchDataAsync(); // Não bloqueante
return View(data);
}Isso requer tornar toda a cadeia de chamadas assíncrona. Cada método que chamar um método assíncrono deve ser assíncrono também. Este é o princípio do "async do começo ao fim".
Padrões Comuns de Deadlock
Padrão 1: .Result em um construtor
// DEADLOCK — construtores não podem ser async
public class DataCache
{
private readonly List<Item> _items;
public DataCache(IDataService service)
{
_items = service.GetItemsAsync().Result; // Gera deadlock em contexto síncrono
}
}
// SOLUÇÃO — usar um método de fábrica
public class DataCache
{
private readonly List<Item> _items;
private DataCache(List<Item> items) => _items = items;
public static async Task<DataCache> CreateAsync(IDataService service)
{
var items = await service.GetItemsAsync();
return new DataCache(items);
}
}Padrão 2: .Wait() em um getter de propriedade
// DEADLOCK
public string CurrentUserName
{
get => GetCurrentUserAsync().Result; // Gera deadlock em apps de UI
}
// SOLUÇÃO — não fazer chamadas assíncronas em getters de propriedades
// Use métodos async em vez disso, ou faça cache do valor
public async Task<string> GetCurrentUserNameAsync()
{
return await _userService.GetCurrentUserAsync();
}Padrão 3: async void com .Wait()
// PROBLEMÁTICO
private async void LoadData()
{
var data = await FetchAsync(); // Ok
Display(data);
}
// Chamando e aguardando
void OnButtonClick()
{
LoadData();
// Não é possível fazer await em async void — sem como saber quando termina
UpdateUI(); // Executa antes de LoadData ser concluído
}Padrão 4: Task.Run para contornar deadlocks
// Solução alternativa (não ideal, mas evita o deadlock)
public string GetData()
{
// Task.Run executa em um thread do thread pool — sem SynchronizationContext
var data = Task.Run(() => FetchDataAsync()).Result;
return data;
}Isso funciona porque Task.Run executa em um thread do thread pool que não possui SynchronizationContext. O await dentro de FetchDataAsync não tem nada para capturar, então a conclusão é postada no thread pool em vez de tentar retornar ao thread bloqueado.
Usar Task.Run como contorno para deadlock é um code smell. Funciona, mas usa threads extras desnecessariamente. Corrija a causa raiz tornando o chamador async.
Quando Deadlocks Não Podem Ocorrer
Deadlocks só ocorrem quando AMBAS as condições são verdadeiras:
- Um SynchronizationContext está presente
- O thread com esse contexto está bloqueado
Sem possibilidade de deadlock em:
- ASP.NET Core (sem SynchronizationContext)
- Aplicações console (sem SynchronizationContext)
- Threads do thread pool (sem SynchronizationContext)
- Código que usa ConfigureAwait(false) em cada ponto de await
Deadlock possível em:
- ASP.NET Clássico (handlers .aspx, Web API 2)
- Handlers de eventos WinForms
- Handlers de eventos WPF
- Qualquer contexto onde
SynchronizationContext.Currentnão seja null E você bloqueie o thread
O sample executa esta tabela como quatro asserções — mesmo método async, mesma chamada bloqueante, alternando um ingrediente por vez:
| Demo | Contexto? | Bloqueio? | Resultado medido |
|---|---|---|---|
| Thread de console simples | não | sim | completa |
| Thread estilo UI | sim | sim | deadlock (observado via timeout de 2s) |
Thread estilo UI + ConfigureAwait(false) | sim | sim | completa |
Thread estilo UI + Task.Run | sim | sim | completa |

Detectando Deadlocks
Quando uma aplicação trava sem exceção, verifique:
- Dump de threads — procure por threads aguardando em
WaitOne,WaitAllouMonitor.Waitenquanto outros threads estão na fila esperando para postar neles - Visual Studio Parallel Stacks — mostra dependências entre threads, tornando deadlocks visíveis
- Adicione um timeout —
.Wait(TimeSpan.FromSeconds(5))retornafalseapós o timeout em vez de travar para sempre, permitindo que você lance uma exceção e torne o bug visível
// Versão de diagnóstico — lança exceção após o timeout em vez de travar
if (!FetchDataAsync().Wait(TimeSpan.FromSeconds(5)))
{
throw new TimeoutException("FetchDataAsync atingiu o timeout — possível deadlock");
}Esse padrão de timeout é exatamente como o sample observa seu deadlock deliberado sem
nunca travar — e tem um bônus de diagnóstico: depois que o Wait esgotado libera o
thread, uma task em deadlock por contexto se completa imediatamente, o que distingue esse
deadlock de uma dependência genuinamente presa.
Resumo
| Cenário | O Que Fazer |
|---|---|
| Código de biblioteca | Sempre use ConfigureAwait(false) |
| ASP.NET Core | Async do começo ao fim (.Result não gera deadlock mas desperdiça threads) |
| ASP.NET Clássico | Async do começo ao fim OU use ConfigureAwait(false) em todo o código |
| WinForms/WPF | Async do começo ao fim; não bloquear o thread de UI |
| Construtores | Usar métodos de fábrica estáticos em vez disso |
| Getters de propriedades | Não fazer chamadas assíncronas a partir de getters de propriedades |
A regra de ouro: nunca bloqueie em código assíncrono. Se você se pegar recorrendo a .Result, .Wait() ou .GetAwaiter().GetResult(), torne o chamador async em vez disso.