1. Introdução
Assincronia em C# é uma ferramenta poderosa. Mas às vezes você precisa lidar com situações em que código assíncrono deve ser chamado a partir de síncrono (ou vice-versa). Parece que tudo deveria "funcionar magicamente", mas na prática podem ocorrer travamentos estranhos (deadlock), perda de desempenho e até — inesperadamente — uma interface do usuário quebrada. Muitas vezes o bug só aparece com dados reais: em produção ou na máquina do usuário. A causa geralmente está na interação incorreta entre código síncrono e assíncrono e em detalhes não óbvios do agendador de tarefas do .NET.
Além disso, bibliotecas e frameworks modernos usam intensamente métodos assíncronos, e você precisa saber como "colar" corretamente código assíncrono nas cadeias síncronas existentes ou, ao contrário, como chamar código síncrono a partir de um método assíncrono.
Resumo: o que acontece quando usamos await
Quando você escreve:
await SomeAsyncMethod();
O código é "quebrado" em duas partes: antes do await e depois. A primeira parte executa até a primeira espera assíncrona (por exemplo, até uma chamada de rede), e a continuação — depois que ela termina. Pergunta: onde a "continuação" será executada? No mesmo thread? Em outro? E se estamos escrevendo uma aplicação desktop (por exemplo, WPF ou WinForms), e se for console? A resposta — depende. E aqui entra em jogo, por exemplo, ConfigureAwait.
O que é SynchronizationContext?
SynchronizationContext é um mecanismo especial do .NET que permite ao código "lembrar" onde e como a continuação de uma operação assíncrona deve ser chamada.
- Em aplicações clássicas WinForms/WPF, SynchronizationContext garante que depois do await o restante do método continuará executando no mesmo thread da UI, para evitar erros de "acesso ao controle a partir do thread errado".
- No ASP.NET (antigo, não Core) SynchronizationContext permite restaurar o HttpContext e continuar lidando com a requisição HTTP.
- Em aplicações console e ASP.NET Core o SynchronizationContext, tipicamente, não existe (é null), e as continuações são executadas no thread pool.
O que é TaskScheduler?
TaskScheduler é um mecanismo de nível mais baixo. Na maioria dos casos você trabalha com TaskScheduler.Default, que usa o thread pool do .NET. Ele é responsável por decidir quais tarefas são executadas, quando e onde.
2. Deadlock ao misturar await e Result/Wait()
Uma das armadilhas mais famosas em C#:
// Em algum código de UI
var result = SomeAsyncMethod().Result;
ou
SomeAsyncMethod().Wait();
Pronto: a aplicação "travou". Por quê?
Como isso acontece?
- Você chama um método assíncrono e imediatamente pede .Result ou .Wait() — ou seja, "bloqueia" o thread atual e espera a tarefa assíncrona terminar.
- SomeAsyncMethod internamente faz await, e agenda a continuação para o mesmo thread via SynchronizationContext, mas esse thread já está bloqueado esperando o Result/.Wait().
- Como o thread está esperando a conclusão do Result/.Wait(), ele não pode executar a continuação.
- Pronto: deadlock — o thread está esperando por ele mesmo.
Isso é fácil de reproduzir em aplicações UI, onde todo o código roda no thread da interface, e todas as continuações aguardam esse thread específico. Em aplicações console e ASP.NET Core (sem contexto de sincronização) esses deadlocks são raros.
Piada da vida: Se você conseguiu um deadlock com .Result — parabéns, você está alguns passos mais perto do título de Senior :D
3. Como embutir corretamente código assíncrono em síncrono?
Recomendação número um: assincronia de cima para baixo
Se você tem um método assíncrono, propague async/await para cima na stack de chamadas até a própria UI ou ponto de entrada. Não mantenha "assincronia pela metade".
Ruim (bloqueia o thread):
// Método síncrono chama assíncrono via .Result
public void DoStuff()
{
var data = GetDataAsync().Result;
// ...
}
Bom (assincronia por toda parte):
public async Task DoStuffAsync()
{
var data = await GetDataAsync();
// ...
}
Se possível — use sempre await, e não .Result / .Wait().
4. Mas existem situações: precisa chamar async a partir de sync
Reescrever toda a árvore superior para async (melhor opção — se você tem essa escolha).
Usar padrões especiais: por exemplo, rodar a tarefa num thread separado via Task.Run, e dentro dele chamar o método async.
public void DoStuff()
{
var result = Task.Run(() => SomeAsyncMethod()).Result;
}
Mas: também há nuances com sincronização em aplicações UI, então é melhor não fazer isso sem necessidade real.
5. O que faz ConfigureAwait(false)?
Às vezes você não precisa que, depois do await, o código continue no mesmo thread (por exemplo, em aplicações server-side ou em uma biblioteca onde o SynchronizationContext não importa). Pelo contrário, é melhor que o .NET não prenda a continuação a um único thread — isso melhora o desempenho!
Sintaxe e princípio de funcionamento
await SomeAsyncMethod().ConfigureAwait(false);
- ConfigureAwait(false) diz: "não preciso do SynchronizationContext original, continua a execução em qualquer lugar, até em outro thread".
- ConfigureAwait(true) (padrão) — "continua a execução no mesmo lugar de onde o await foi chamado, preferencialmente no mesmo SynchronizationContext".
Esquema visual
┌─────────────────────────────────────────────────────┐
│ SynchronizationContext │
└─────────────────────────────────────────────────────┘
↑ ↑
(UI-thread) await SomeAsyncMethod() Continuation (depois do await)
──────────────────────────────> (mesmo thread — se ConfigureAwait(true))
↓
(qualquer thread — se ConfigureAwait(false))
Exemplo de uso de ConfigureAwait(false) — código de biblioteca
Imagine que você está escrevendo uma biblioteca que pode ser usada por qualquer um: WinForms, WPF, ASP.NET, aplicações console…
Você não deve depender do modelo de threads deles. Por isso sempre use ConfigureAwait(false) nos métodos assíncronos da sua biblioteca:
public async Task<string> LoadDataFromUrlAsync(string url)
{
using var client = new HttpClient();
string content = await client.GetStringAsync(url).ConfigureAwait(false);
return content;
}
Agora seu método não irá "exigir" execução em um SynchronizationContext específico. Isso é mais seguro e mais performático (menos trocas de thread).
Exemplo: o que acontece com await sem ConfigureAwait
Considere uma aplicação WPF:
private async void Button_Click(object sender, RoutedEventArgs e)
{
Button1.Content = "Carregando...";
await Task.Delay(2000); // Simula operação longa
Button1.Content = "Pronto!";
}
Task.Delay por dentro faz "await". Por padrão, depois do await o controle volta para o thread da UI, permitindo continuar atualizando controles.
Se dentro da sua operação longa você usar ConfigureAwait(false):
await Task.Delay(2000).ConfigureAwait(false);
Button1.Content = "Pronto!"; // Erro!
Vai ocorrer uma exceção: InvalidOperationException: "The calling thread cannot access this object because a different thread owns it."
Porque agora a "continuação" roda em outro thread, e não se pode acessar a UI.
Conclusão: use ConfigureAwait(false) apenas onde não é necessário acessar UI/contexto.
6. Nuances úteis
Onde e quando usar ConfigureAwait
| Cenário | Deve usar .ConfigureAwait(false)? | Por quê? |
|---|---|---|
| Código de biblioteca | Sim | Pode ser chamado em qualquer contexto, UI não é necessário |
| No código dentro do ASP.NET Core | Sim (contexto praticamente não existe) | Aumenta o desempenho |
| Em WinForms/WPF, ao acessar a UI | Não | É preciso voltar o controle para o thread da UI |
| Métodos síncronos | Sem sentido | Não há SynchronizationContext |
| Em aplicações console | Pode, mas sem efeito visível | Não há contexto |
Como descobrir se existe um SynchronizationContext?
Você pode checar diretamente no código:
Console.WriteLine(SynchronizationContext.Current == null
? "Sem contexto"
: "Contexto existe");
- Em aplicações console e ASP.NET Core será "Sem contexto".
- Em WinForms/WPF — "Contexto existe".
Visualização da troca de threads
sequenceDiagram
participant MainThread as Thread principal (UI/Console)
participant ThreadPool as Thread do pool
MainThread->>SomeAsyncMethod: Chama o método
SomeAsyncMethod->>MainThread: await sem ConfigureAwait
Note right of MainThread: Depois do await voltamos para o mesmo thread
SomeAsyncMethod->>ThreadPool: await com ConfigureAwait(false)
Note right of ThreadPool: Depois do await podemos trabalhar em qualquer thread
Regras rápidas de uso
- Não use .Result e .Wait() em código de UI com métodos assíncronos.
- No código de biblioteca use rigorosamente ConfigureAwait(false) para todos os awaits.
- Em aplicações UI use ConfigureAwait(false) apenas dentro de métodos que não acessam elementos da interface.
- Evite misturar código síncrono e assíncrono sem necessidade.
- Se precisar chamar async a partir de sync — pense duas vezes: não dá para transformar toda a cadeia em async?
7. Erros típicos de iniciantes ao trabalhar com assincronia
Erro nº1: uso generalizado de .Result ou .Wait().
Muitos desenvolvedores, especialmente depois de ler alguns artigos sobre assincronia, começam a adicionar .Result ou .Wait() em todo lugar para "sincronizar" chamadas assíncronas. À primeira vista parece conveniente, mas na prática é um caminho garantido para deadlocks em aplicações UI. Isso é especialmente perigoso se dentro dos métodos assíncronos não for usado ConfigureAwait(false) — o thread da UI fica bloqueado e não pode executar a continuação da tarefa.
Erro nº2: uso incorreto ou excessivo de ConfigureAwait(false).
Alguns iniciantes aplicam ConfigureAwait(false) em todo lugar, inclusive em código que manipula elementos da interface. Em aplicações UI isso leva a erros de acesso aos controles (InvalidOperationException), porque a continuação passa a executar em um thread diferente do thread da UI.
Erro nº3: esquecer que ConfigureAwait funciona só com objetos awaitable.
Muitos pensam que podem usar ConfigureAwait(false) em qualquer método, mas na verdade ele funciona apenas com objetos que retornam Task, Task<T>, ValueTask e outros tipos awaitable. Para métodos síncronos ConfigureAwait não tem efeito, e esperar por esses métodos não se torna assíncrono.
Erro nº4: misturar código síncrono e assíncrono sem necessidade.
Tentar chamar um método assíncrono de código síncrono sem uma estratégia pensada (por exemplo, via .Result, .Wait() ou Task.Run) frequentemente leva a bugs sutis, perda de desempenho e deadlocks difíceis de diagnosticar. Mesmo que o método pareça curto e seguro, tais chamadas na cadeia podem quebrar toda a aplicação.
Erro nº5: subestimar a influência do SynchronizationContext.
Iniciantes frequentemente esquecem que a continuação após await pode executar no mesmo thread (UI), se não usar ConfigureAwait(false). Isso leva a comportamento imprevisível, especialmente ao combinar código de UI e código de biblioteca, onde esperas e continuações se entrelaçam.
GO TO FULL VERSION