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 void a Todo Custo Métodos com retorno async void não podem ser aguardados com await, e qualquer exceção não tratada dentro deles derruba todo o processo da aplicação (crash incondicional). Use async void exclusivamente em manipuladores de eventos de UI legado (WPF/WinForms). Em qualquer serviço ou backend, utilize sempre async Task ou async 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).");
    }
}