Módulo 1 · C# — Capítulo 10

Async/Await

Você já usa async/await há anos em Node.js — a boa notícia é que a sintaxe é quase idêntica. As diferenças que importam estão por baixo dos panos.

1. A boa notícia: a sintaxe já é familiar

JS Node.js
async function obterCliente(id) {
    const resposta = await fetch(`/clientes/${id}`);
    const cliente = await resposta.json();
    return cliente;
}
C# C#
async Task<Cliente> ObterClienteAsync(int id)
{
    var resposta = await httpClient.GetAsync($"/clientes/{id}");
    var cliente = await resposta.Content.ReadFromJsonAsync<Cliente>();
    return cliente!;
}

Três diferenças de sintaxe, todas superficiais:

JavaScriptC#
Promise<T> (implícito no retorno)Task<T> explícito no tipo de retorno
Nome do método sem convenção fixaConvenção: sufixo Async no nome (ObterClienteAsync)
Promise.all([...])await Task.WhenAll(...)

2. Task vs. Task<T> vs. void

Tipo de retornoEquivalente JSQuando usar
TaskPromise<void>Método assíncrono que não devolve valor
Task<T>Promise<T>Método assíncrono que devolve um valor do tipo T
async voidEvite — só é aceitável em handlers de evento (ex: clique de botão em UI)
Atenção

async void não pode ser aguardado (await) por quem chama, e uma exceção lançada dentro dele derruba o processo em vez de propagar normalmente. Fora do caso específico de handlers de evento em UI, sempre use async Task ou async Task<T>.

3. O que realmente muda: threads, não sintaxe

Node.js é single-threaded — async/await ali existe para não bloquear o único loop de eventos enquanto se espera I/O. C#/.NET é multi-thread por natureza, e async/await existe para não bloquear a thread atual (que pode ser uma entre várias) enquanto se espera uma operação — a thread fica livre para fazer outro trabalho nesse meio tempo, e retoma quando a operação awaited terminar.

Sem await (bloqueante) Thread parada esperando o banco responder Com await Chama o banco Thread livre p/ outra requisição Numa API que atende várias requisições ao mesmo tempo, isso é a diferença entre suportar dezenas ou milhares de requisições simultâneas com os mesmos recursos.

Fig. 1 — await libera a thread durante a espera; ela volta a ser usada assim que a operação termina.

4. ConfigureAwait e o contexto de sincronização

Em código de biblioteca (não em endpoints de API ASP.NET Core), é comum ver .ConfigureAwait(false) depois de um await. Isso diz ao runtime "não precisa retomar necessariamente na mesma thread/contexto de origem" — uma otimização que evita deadlocks em certos cenários mais antigos (WPF/WinForms, capítulo do Módulo 6). Em APIs ASP.NET Core modernas isso não é mais estritamente necessário, mas ainda aparece bastante em código de bibliotecas reutilizáveis.

5. Executando tarefas em paralelo

C# Task.WhenAll — como Promise.all
Task<Cliente> tarefaCliente = ObterClienteAsync(1);
Task<List<Pedido>> tarefaPedidos = ObterPedidosAsync(1);

await Task.WhenAll(tarefaCliente, tarefaPedidos);

Cliente cliente = tarefaCliente.Result;         // já concluída, seguro ler .Result aqui
List<Pedido> pedidos = tarefaPedidos.Result;
Nunca misture .Result/.Wait() com await no mesmo fluxo

Chamar .Result ou .Wait() numa Task ainda não concluída bloqueia a thread — exatamente o problema que async/await existe para evitar — e em certos contextos (UI, ASP.NET clássico) pode travar a aplicação inteira num deadlock. Sempre prefira await de ponta a ponta.

6. Async "até o fim" — por que não dá para misturar

Uma vez que um método é async, essa natureza tende a se propagar para quem o chama — daí a expressão "async all the way down". Tentar "voltar a ser síncrono" no meio do caminho (chamando .Result) reintroduz justamente o bloqueio que se queria evitar.

7. Onde isso te leva

Você já usa async/await desde o Módulo 2 (todos os exemplos de integração com C# eram assíncronos). O próximo capítulo cobre delegates, eventos e expressões lambda — as peças que tornam possível passar "comportamento" como se fosse dado, algo que você já faz naturalmente em JavaScript com funções de primeira classe.

📌 Resumo do capítulo

  • A sintaxe de async/await em C# é quase idêntica à do JavaScript; Task/Task<T> equivalem a Promise/Promise<T>.
  • Diferente do Node (single-thread), .NET é multi-thread — await libera a thread atual para outro trabalho durante a espera.
  • Evite async void fora de handlers de evento de UI.
  • Task.WhenAll é o equivalente de Promise.all.
  • Nunca misture .Result/.Wait() com código assíncrono — isso reintroduz bloqueio e pode causar deadlock.

✏️ Praticando

  1. Escreva um método Task<Cliente> ObterClienteAsync(int id) que simula uma chamada lenta com await Task.Delay(1000) antes de devolver um objeto fixo.
  2. Chame dois desses métodos (Ids diferentes) em paralelo com Task.WhenAll e meça o tempo total — compare com chamar os dois sequencialmente, um await depois do outro.
  3. Explique, em suas próprias palavras, por que .Result pode causar deadlock em certos contextos.