Capítulo 17: Programação Assíncrona com Async/Await, Task e CancellationToken
Capítulo 17: Programação Assíncrona com Async/Await, Task e CancellationToken
Especialização em Sistemas com C# • C# 12 & .NET 8 • POO Moderna, LINQ, Concorrência e Arquitetura Corporativa
🗺️ Mapa Conceitual da Máquina de Estados Assíncrona e Fluxo de Execução
flowchart TD
Inicio["Método async Inicia na Thread Chamadora"] --> Check{"A Operação I/O concluiu imediatamente?"}
Check -- "Sim (Síncrono/Cache)" --> FastPath["Fast-Path: Retorna ValueTask sem suspender a Thread"]
Check -- "Não (I/O Pendente)" --> CriarStateMachine["Aloca / Prepara IAsyncStateMachine"]
CriarStateMachine --> RegistrarIOCP["Registra Callback no I/O Completion Port (IOCP) do SO"]
RegistrarIOCP --> LiberarThread["Libera a Thread de volta para o ThreadPool!"]
RegistrarIOCP -.->|"Hardware/Rede Conclui Leitura"| SOCompleta["SO Sinaliza I/O Completion Port"]
SOCompleta --> ThreadPoolQueue["Acorda e Enfileira Continuação no ThreadPool"]
ThreadPoolQueue --> RetomarStateMachine["Uma Thread Disponível Retoma o Ponto após o Await"]
RetomarStateMachine --> Fim["Retorno da Task<T> Concluída com Sucesso"]
style FastPath fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style LiberarThread fill:#fff3e0,stroke:#ff9800,stroke-width:2px
style CriarStateMachine fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
style SOCompleta fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
🏛️ 1. O Modelo Mental Assíncrono: Assincronismo vs Paralelismo
Em desenvolvimento de software moderno de alto desempenho, assincronismo não significa executar código em outra thread; significa liberar a thread atual enquanto se aguarda uma resposta externa (I/O de rede, disco ou banco).
| Característica | Multithreading / Paralelismo | Programação Assíncrona (Async/Await) |
|---|---|---|
| Gargalo Típico | Processamento intensivo de CPU (cálculos matemáticos, renderização) | Operações bloqueantes de I/O (APIs HTTP, Queries SQL, Disco) |
| Recurso Utilizado | Múltiplos núcleos da CPU executando ativamente | Liberação de threads de volta para o ThreadPool |
| Consumo de Memória | Alto (~1 MB por pilha de thread nativa no Windows) | Mínimo (apenas o estado da struct/classe state machine) |
| Abstração C# | Parallel.ForEach, Task.Run com workers de CPU |
async / await, Task, ValueTask, CancellationToken |
⚙️ 2. A Máquina de Estados da CLR (IAsyncStateMachine)
Quando o compilador C# encontra a palavra-chave async, ele decompõe o método em uma struct que implementa IAsyncStateMachine. O método original é reescrito para alternar entre estados inteiros (0, 1, 2…) toda vez que um await é atingido.
// O que você escreve:
public async Task<string> ObterDadosAsync(string url)
{
var resposta = await _http.GetStringAsync(url);
return resposta.ToUpperInvariant();
}
// O que o compilador C# gera (visão conceitual desmistificada):
// Uma struct com campos para guardar variáveis locais e um método MoveNext()
// que é invocado pelo Awaiter quando o I/O completa.
[!WARNING] Evite
async voida Todo Custo Métodos com retornoasync voidnão podem ser aguardados comawait, e qualquer exceção não tratada dentro deles derruba todo o processo da aplicação (crash incondicional). Useasync voidexclusivamente em manipuladores de eventos de UI legado (WPF/WinForms). Em qualquer serviço ou backend, utilize sempreasync Taskouasync ValueTask.
⚡ 3. Task vs ValueTask: Otimização Zero Alocação
A classe Task é um tipo de referência (alocado na Heap). Em serviços de microsserviços de alto throughput (ex: 50.000 requisições/segundo), instanciar uma Task para operações que frequentemente completam em cache gera pressão desnecessária sobre o Garbage Collector (GC Gen 0).
A struct ValueTask<T> resolve isso:
- Se a operação completou de forma síncrona (ex: dado encontrado no cache em memória), nenhum objeto é alocado na Heap.
- Se a operação requerer I/O assíncrono real, ela encapsula uma
Task<T>internamente.
public ValueTask<string> ObterConfiguracaoAsync(string chave)
{
// Fast-path: Se já está em cache, retorna síncrono sem alocar Task!
if (_cacheEmMemoria.TryGetValue(chave, out string? valor))
{
return new ValueTask<string>(valor);
}
// Slow-path: Consulta assíncrona real ao banco/disco
return new ValueTask<string>(ConsultarBancoAsync(chave));
}
🛡️ 4. Cancelamento Cooperativo com CancellationToken
Em sistemas distribuídos, uma requisição do cliente pode ser cancelada pelo usuário ou atingir um timeout de gateway. O .NET implementa o padrão de cancelamento cooperativo através de CancellationTokenSource e CancellationToken.
// Propagação obrigatória do token em toda a cadeia de chamadas:
public async Task ProcessarPagamentoAsync(Guid pedidoId, CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested(); // Interrupção precoce antes de I/O pesado
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
cts.CancelAfter(TimeSpan.FromSeconds(5)); // Timeout de 5s para o gateway
await _gatewayClient.EfetuarCobrancaAsync(pedidoId, cts.Token);
}
💻 5. Laboratório Prático de Arquitetura: Pipeline Assíncrono com Agregação e Cancelamento
Abaixo está uma demonstração executável demonstrando orquestração paralela com Task.WhenAll, streaming assíncrono com IAsyncEnumerable<T> e cancelamento gracioso via timeout.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
namespace TreinamentoDotNet.Concorrencia;
public record CotacaoMoeda(string Codigo, decimal ValorBRL, DateTime AtualizadoEm);
public class ServicoCambioIntegrado
{
// 1. Streaming Assíncrono Reativo (C# 8+)
public async IAsyncEnumerable<CotacaoMoeda> TransmitirCotacoesEmTempoRealAsync(
[EnumeratorCancellation] CancellationToken ct = default)
{
string[] moedas = ["USD", "EUR", "GBP", "JPY", "CHF"];
var random = new Random();
foreach (var moeda in moedas)
{
ct.ThrowIfCancellationRequested();
// Simula latência de conexão de streaming
await Task.Delay(100, ct);
decimal cotacaoBase = moeda switch
{
"USD" => 5.25m,
"EUR" => 5.70m,
"GBP" => 6.65m,
"JPY" => 0.035m,
_ => 5.80m
};
yield return new CotacaoMoeda(
moeda,
cotacaoBase + (decimal)(random.NextDouble() * 0.1 - 0.05),
DateTime.UtcNow
);
}
}
// 2. Consulta Concorrente com Resiliência e Agregação
public async Task<IReadOnlyList<CotacaoMoeda>> ObterCotacoesParalelasAsync(
IEnumerable<string> moedas,
CancellationToken ct = default)
{
var tarefas = moedas.Select(moeda => ConsultarParceiroAsync(moeda, ct));
// Executa todas as consultas I/O simultaneamente
CotacaoMoeda[] resultados = await Task.WhenAll(tarefas);
return resultados.OrderByDescending(c => c.ValorBRL).ToList();
}
private async Task<CotacaoMoeda> ConsultarParceiroAsync(string moeda, CancellationToken ct)
{
// Simula I/O assíncrono de rede
await Task.Delay(150, ct);
return new CotacaoMoeda(moeda, 5.0m + moeda.Length, DateTime.UtcNow);
}
}
public class Program
{
public static async Task Main()
{
Console.WriteLine("=== Laboratório: Concorrência Assíncrona no .NET 8 ===");
var servico = new ServicoCambioIntegrado();
// Cenário 1: Streaming com IAsyncEnumerable
Console.WriteLine("\n[1] Consumindo Streaming IAsyncEnumerable com CancellationToken:");
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
try
{
await foreach (var item in servico.TransmitirCotacoesEmTempoRealAsync(cts.Token))
{
Console.WriteLine($" -> Recebido: {item.Codigo} = R$ {item.ValorBRL:F4} às {item.AtualizadoEm:HH:mm:ss.fff}");
}
}
catch (OperationCanceledException)
{
Console.WriteLine(" -> Streaming interrompido por cancelamento ou timeout.");
}
// Cenário 2: Orquestração Concorrente com Task.WhenAll
Console.WriteLine("\n[2] Orquestração em Paralelo via Task.WhenAll:");
var sw = Stopwatch.StartNew();
var listaMoedas = new[] { "USD", "EUR", "CAD", "AUD", "NZD" };
var cotacoes = await servico.ObterCotacoesParalelasAsync(listaMoedas, CancellationToken.None);
sw.Stop();
foreach (var c in cotacoes)
{
Console.WriteLine($" -> {c.Codigo}: R$ {c.ValorBRL:F2}");
}
Console.WriteLine($" -> Tempo total de 5 consultas paralelas: {sw.ElapsedMilliseconds}ms (esperado ~150ms).");
}
}