Como chamar um método assíncrono a partir de um síncrono em C#? Carne, não água
Imagina: você está à noite codando, finalizando um utilitário de console antigo em C#. Tudo funciona de forma síncrona, como um relógio. E então — puf! — você precisa chamar uma API que foi feita apenas com async Task. Não vai reescrever o programa inteiro, né? Dor familiar, certo?
A pergunta «How to call asynchronous method from synchronous method in C#» é uma das mais populares no Stack Overflow. E não é à toa. Lá, nos comentários, toneladas de conselhos desde «faz .Result e não se preocupa» até «você é louco, vai ter um deadlock!». Vamos organizar por tópicos: quais abordagens realmente funcionam e quais são o caminho para o inferno.
Por que simplesmente «await» não funciona?
Se você tentar escrever:
public void SyncMethod(){ await DoSomethingAsync(); // Erro CS4032}O compilador simplesmente se recusará a compilar. await só pode ser usado dentro de um método marcado com o modificador async. E async void é uma outra história que também vamos abordar.
Então, temos um método assíncrono:
public async Task<string> GetDataAsync(){ using var httpClient = new HttpClient(); return await httpClient.GetStringAsync("https://api.example.com/data");}E temos um método síncrono, a partir do qual queremos chamá-lo. Vamos às opções.
Abordagem 1: GetAwaiter().GetResult() — estilo hacker
A maneira mais popular e, talvez, a mais elegante:
public string CallSync(){ return GetDataAsync().GetAwaiter().GetResult();}Por que é legal:
- Não gera a exceção
AggregateException(ao contrário de.Result). Se dentro do método assíncrono ocorrer um erro, você receberá o tipo original, não um wrapper. - O código é legível, sem try-catch desnecessários.
Mas tem um porém: se o seu método síncrono for executado em um contexto com SynchronizationContext (por exemplo, na thread de UI do WinForms ou WPF), você corre o risco de ter um deadlock — um bloqueio mútuo de threads. A thread A espera a conclusão da tarefa assíncrona, e a tarefa não pode terminar porque espera que a thread A seja liberada. Fodeu.
Como evitar: Use .ConfigureAwait(false) no próprio método assíncrono. Mas isso já altera a API, não apenas o código de chamada.
Abordagem 2: Task.Run(async () => await ...).GetAwaiter() — jogar no pool de threads
Se você não pode alterar o método assíncrono, mas quer evitar garantidamente o deadlock — jogue a tarefa no pool de threads:
public string CallSyncSafe(){ return Task.Run(async () => await GetDataAsync()).GetAwaiter().GetResult();}Mágica: Task.Run inicia o delegate em uma thread do ThreadPool, que não tem SynchronizationContext. Deadlock impossível. Mesmo que o método original use ConfigureAwait(true).
Desvantagem: Uma troca de contexto extra (context switch). Para centenas de chamadas — não é legal. Para chamadas únicas — ideal.
Abordagem 3: .Result ou .Wait() — método da vovó
public string CallSyncOldSchool(){ return GetDataAsync().Result; // Ou .Wait()}O que há de ruim:
- Em caso de erro, lança
AggregateException. Para chegar ao erro real, você precisa fuçar noInnerException. - O mesmo risco de deadlock em contexto de UI.
- O código parece uma gambiarra de 2012.
Essa abordagem é uma bandeira vermelha para code review. Se eu vir isso em um pull request — vou barrar.
Abordagem 4: AsyncHelper ou RunSync customizado
Desenvolvedores experientes às vezes escrevem seus próprios helpers. Por exemplo, assim: