Chamada assíncrona a partir de código síncrono em C#: batalha de abordagens

Treinador Online de Python para Iniciantes

Aprenda Python facilmente sem sobrecarga teórica. Resolva tarefas práticas com verificação automática e escreva código diretamente no navegador.

Start Course

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 no InnerException.
  • 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:

                

Blogs

Book Recommendations