CodeGym /Cursos /C# SELF /Interação entre código assíncrono e síncrono

Interação entre código assíncrono e síncrono

C# SELF
Nível 62 , Lição 4
Disponível

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?

  1. 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.
  2. 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().
  3. Como o thread está esperando a conclusão do Result/.Wait(), ele não pode executar a continuação.
  4. 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.

1
Pesquisa/teste
Fluxos de dados assíncronos, nível 62, lição 4
Indisponível
Fluxos de dados assíncronos
Mergulho profundo em assincronia
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION