Introdução

Se você desenvolve aplicações com C# e .NET, já encontrou os termos async, await, Task e CancellationToken espalhados por todo o código — especialmente em APIs ASP.NET Core. Mas será que você realmente entende o que acontece quando marca um método como async? E mais importante: programação assíncrona é a mesma coisa que paralelismo? A resposta curta é não, e confundir esses dois conceitos é um dos erros mais comuns entre desenvolvedores.

Neste artigo, vamos descomplicar a programação assíncrona em C# de forma prática e didática. Você vai entender a diferença fundamental entre código assíncrono e paralelo, como threads funcionam (incluindo o que significa ser thread-safe), os benefícios reais de usar async/await corretamente, e por que o CancellationToken é essencial para construir aplicações responsivas e resilientes — inclusive nas controllers do ASP.NET Core. Ao final, você terá exemplos completos que pode copiar, colar e executar para sentir na prática a diferença entre síncrono e assíncrono.

Este conteúdo é voltado para desenvolvedores back-end e full-stack que trabalham com .NET e desejam escrever código assíncrono correto, performático e seguro. Se você já domina conceitos de segurança em APIs, como Autenticação e Autorização com JWT, OAuth2 e OpenID Connect, ou utiliza automações no dia a dia como descrito em Makefile: Automatizando Python, Hugo e Docker, entender programação assíncrona vai elevar ainda mais a qualidade do seu código.

📦 Código-fonte: A implementação completa deste artigo está no repositório blog-zocateli-sample no GitHub. Clone, explore e adapte ao seu contexto.


Assíncrono Não é Paralelismo

Este é o ponto mais importante deste artigo, e precisa ficar claro desde o início: programação assíncrona e paralelismo são conceitos diferentes. Muitos desenvolvedores usam os termos como sinônimos, mas eles resolvem problemas distintos.

O que é Programação Assíncrona?

Programação assíncrona é sobre não bloquear a thread atual enquanto espera uma operação demorada terminar. Quando você faz uma chamada HTTP, lê um arquivo do disco ou consulta um banco de dados, essas operações envolvem I/O (entrada/saída) — e o processador fica ocioso esperando a resposta. Com código assíncrono, a thread é liberada para fazer outras coisas enquanto a operação de I/O acontece.

O que é Paralelismo?

Paralelismo é sobre executar múltiplas tarefas simultaneamente, usando múltiplos núcleos do processador. É útil para operações CPU-bound — tarefas que realmente precisam de processamento pesado, como cálculos matemáticos, compressão de imagens ou processamento de dados.

A Analogia do Restaurante

Imagine um restaurante com um garçom (a thread):

  • Síncrono: O garçom anota o pedido de uma mesa, vai até a cozinha, fica parado esperando o prato ficar pronto, volta e entrega. Só então atende a próxima mesa. Se o prato demora 20 minutos, o garçom fica 20 minutos parado.
  • Assíncrono: O garçom anota o pedido, entrega na cozinha e vai atender outras mesas enquanto o prato é preparado. Quando o prato fica pronto, ele busca e entrega. Um único garçom atende várias mesas.
  • Paralelo: O restaurante contrata vários garçons (várias threads) para atender mesas ao mesmo tempo. Cada garçom trabalha independentemente.

Ilustração comparando um único garçom atendendo várias mesas (assíncrono) com vários garçons trabalhando ao mesmo tempo (paralelo)

ℹ️ Informação: Assíncrono é sobre eficiência — fazer mais com menos recursos. Paralelismo é sobre velocidade — fazer mais coisas ao mesmo tempo usando mais recursos. Em APIs web, o assíncrono é quase sempre o que você quer; paralelismo é para cenários específicos de processamento pesado.

Comparação Direta

AspectoAssíncronoParalelo
Problema que resolveOperações de I/O (rede, disco, banco)Processamento pesado (CPU)
ThreadsLibera a thread durante a esperaUsa múltiplas threads simultaneamente
Recurso principalasync/await, TaskParallel.ForEach, Task.Run, PLINQ
ExemploChamada HTTP, leitura de arquivoCálculo de hash, compressão de imagem
Quando usarAPIs web, I/O-boundProcessamento batch, CPU-bound

Como Threads Funcionam em C#

Para entender programação assíncrona, é fundamental entender o que são threads e como o .NET as gerencia.

O que é uma Thread?

Uma thread é a menor unidade de execução de um programa. Quando sua aplicação .NET inicia, ela recebe pelo menos uma thread — a main thread. Cada thread tem sua própria pilha de execução (stack), mas todas compartilham o mesmo espaço de memória (heap) do processo.

O .NET utiliza o ThreadPool — um pool gerenciado de threads reutilizáveis. Em vez de criar e destruir threads constantemente (o que é caro), o runtime mantém um conjunto de threads prontas para uso. Quando uma operação assíncrona completa, uma thread do pool é designada para continuar a execução.

Thread-Safe vs Thread-Unsafe

Quando múltiplas threads acessam o mesmo recurso compartilhado (uma variável, lista, dicionário), podem ocorrer race conditions — situações onde o resultado depende da ordem de execução das threads, que é imprevisível.

Exemplo Thread-Unsafe (Race Condition)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// ❌ PROBLEMA: Este código NÃO é thread-safe
// Múltiplas continuações assíncronas acessam a mesma variável sem proteção

var contador = 0;

// Método assíncrono que modifica estado compartilhado
async Task IncrementarSemProtecaoAsync()
{
    await Task.Yield(); // Força continuação em outra thread do pool
    contador++;         // ❌ Race condition: não é uma operação atômica
}

// Dispara 1000 chamadas assíncronas concorrentes
var tasks = Enumerable.Range(0, 1000)
    .Select(_ => IncrementarSemProtecaoAsync());

await Task.WhenAll(tasks);

Console.WriteLine($"Esperado: 1000, Obtido: {contador}");
// Output pode ser: Esperado: 1000, Obtido: 987 (ou outro valor < 1000)

O contador++ parece uma operação simples, mas internamente são três passos: ler o valor, somar 1, gravar o novo valor. Se duas threads leem o valor 500 ao mesmo tempo, ambas gravam 501, e um incremento é perdido. Isso acontece porque contador++ não é uma operação atômica.

Exemplo Thread-Safe (com lock)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// ✅ CORRETO: Acesso protegido com lock
var contador = 0;
var lockObj = new object();

async Task IncrementarComLockAsync()
{
    await Task.Yield();
    lock (lockObj)
    {
        contador++; // Apenas uma thread por vez executa este bloco
    }
}

var tasks = Enumerable.Range(0, 1000)
    .Select(_ => IncrementarComLockAsync());

await Task.WhenAll(tasks);

Console.WriteLine($"Esperado: 1000, Obtido: {contador}");
// Output: Esperado: 1000, Obtido: 1000 ✅

O que São Operações Atômicas?

Uma operação atômica é uma operação que se completa inteiramente ou não acontece — não existe meio-termo. Do ponto de vista de outras threads, a operação é instantânea: nenhuma thread consegue observar o estado intermediário.

O problema com contador++ é que ele não é atômico. São três operações separadas:

  1. Ler o valor atual de contador da memória.
  2. Somar 1 ao valor lido.
  3. Gravar o novo valor de volta na memória.

Entre o passo 1 e o passo 3, outra thread pode ler o mesmo valor antigo — gerando a race condition que vimos acima. Uma operação atômica garante que esses três passos aconteçam como se fossem um só, sem que outra thread consiga interferir no meio do caminho.

O .NET oferece operações atômicas prontas para uso na classe Interlocked, que utiliza instruções especiais do processador (como lock cmpxchg no x86) para garantir atomicidade sem precisar de lock — o que torna o código mais rápido em cenários de alta concorrência.

OperaçãoMétodo InterlockedEquivalente não-atômico
IncrementarInterlocked.Increment(ref x)x++
DecrementarInterlocked.Decrement(ref x)x--
Somar valorInterlocked.Add(ref x, valor)x += valor
Trocar valorInterlocked.Exchange(ref x, novo)x = novo
Trocar se igualInterlocked.CompareExchange(ref x, novo, esperado)if (x == esperado) x = novo

Exemplo Thread-Safe (com Interlocked)

Para operações simples como incremento, o Interlocked é mais eficiente que lock porque não bloqueia outras threads — a operação é atômica no nível do hardware:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// ✅ CORRETO e mais performático: Interlocked para operações atômicas
var contador = 0;

async Task IncrementarAtomicoAsync()
{
    await Task.Yield();
    Interlocked.Increment(ref contador); // ✅ Operação atômica — sem lock
}

var tasks = Enumerable.Range(0, 1000)
    .Select(_ => IncrementarAtomicoAsync());

await Task.WhenAll(tasks);

Console.WriteLine($"Esperado: 1000, Obtido: {contador}");
// Output: Esperado: 1000, Obtido: 1000 ✅

ℹ️ Informação: Use lock quando precisa proteger um bloco de código com múltiplas operações (ler + modificar + gravar uma estrutura complexa). Use Interlocked quando precisa proteger uma única variável com operações simples (incremento, troca). O Interlocked é entre 10× a 50× mais rápido que lock para essas operações individuais.

Coleções Thread-Safe do .NET

O .NET oferece coleções no namespace System.Collections.Concurrent projetadas para acesso seguro por múltiplas threads:

Coleção Thread-UnsafeEquivalente Thread-SafeQuando usar
List<T>ConcurrentBag<T>Coleção não ordenada
Dictionary<K,V>ConcurrentDictionary<K,V>Dicionário com acesso concorrente
Queue<T>ConcurrentQueue<T>Fila produtor/consumidor
Stack<T>ConcurrentStack<T>Pilha com acesso concorrente

💡 Dica: Se você está usando async/await puro (sem Task.Run ou Parallel), em muitos cenários não precisa de coleções thread-safe, pois o código assíncrono geralmente executa de forma sequencial em uma única thread de contexto. A preocupação com thread safety surge quando você introduz paralelismo real.


async/await na Prática: Síncrono vs Assíncrono

Vamos ao que interessa: código que você pode executar e sentir a diferença. Crie um projeto console para testar:

1
2
dotnet new console -n AsyncDemo
cd AsyncDemo

Exemplo 1: Código Síncrono (Bloqueante)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Program.cs — Versão SÍNCRONA
using System.Diagnostics;

var sw = Stopwatch.StartNew();

Console.WriteLine("Iniciando operações SÍNCRONAS...\n");

// Simula 3 chamadas HTTP que demoram 2 segundos cada
var resultado1 = BuscarDadosSincrono("Serviço A", 2000);
var resultado2 = BuscarDadosSincrono("Serviço B", 2000);
var resultado3 = BuscarDadosSincrono("Serviço C", 2000);

Console.WriteLine($"\n{resultado1}");
Console.WriteLine(resultado2);
Console.WriteLine(resultado3);

sw.Stop();
Console.WriteLine($"\n⏱️ Tempo total: {sw.ElapsedMilliseconds}ms");
// Output esperado: ~6000ms (2s + 2s + 2s — sequencial)

static string BuscarDadosSincrono(string servico, int delayMs)
{
    Console.WriteLine($"  [{DateTime.Now:HH:mm:ss.fff}] Buscando {servico}...");
    Thread.Sleep(delayMs); // ❌ Bloqueia a thread
    Console.WriteLine($"  [{DateTime.Now:HH:mm:ss.fff}] {servico} concluído.");
    return $"Dados do {servico}";
}

Output esperado:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Iniciando operações SÍNCRONAS...

  [19:00:00.001] Buscando Serviço A...
  [19:00:02.003] Serviço A concluído.
  [19:00:02.004] Buscando Serviço B...
  [19:00:04.006] Serviço B concluído.
  [19:00:04.006] Buscando Serviço C...
  [19:00:06.008] Serviço C concluído.

Dados do Serviço A
Dados do Serviço B
Dados do Serviço C

⏱️ Tempo total: 6015ms

Exemplo 2: Código Assíncrono (Não-Bloqueante)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Program.cs — Versão ASSÍNCRONA
using System.Diagnostics;

var sw = Stopwatch.StartNew();

Console.WriteLine("Iniciando operações ASSÍNCRONAS...\n");

// Inicia todas as 3 chamadas ao mesmo tempo
var task1 = BuscarDadosAsync("Serviço A", 2000);
var task2 = BuscarDadosAsync("Serviço B", 2000);
var task3 = BuscarDadosAsync("Serviço C", 2000);

// Aguarda TODAS completarem (sem bloquear a thread)
var resultados = await Task.WhenAll(task1, task2, task3);

Console.WriteLine($"\n{resultados[0]}");
Console.WriteLine(resultados[1]);
Console.WriteLine(resultados[2]);

sw.Stop();
Console.WriteLine($"\n⏱️ Tempo total: {sw.ElapsedMilliseconds}ms");
// Output esperado: ~2000ms (todas executam "ao mesmo tempo")

static async Task<string> BuscarDadosAsync(string servico, int delayMs)
{
    Console.WriteLine($"  [{DateTime.Now:HH:mm:ss.fff}] Buscando {servico}...");
    await Task.Delay(delayMs); // ✅ Libera a thread durante a espera
    Console.WriteLine($"  [{DateTime.Now:HH:mm:ss.fff}] {servico} concluído.");
    return $"Dados do {servico}";
}

Output esperado:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Iniciando operações ASSÍNCRONAS...

  [19:00:00.001] Buscando Serviço A...
  [19:00:00.002] Buscando Serviço B...
  [19:00:00.002] Buscando Serviço C...
  [19:00:02.005] Serviço A concluído.
  [19:00:02.005] Serviço B concluído.
  [19:00:02.006] Serviço C concluído.

Dados do Serviço A
Dados do Serviço B
Dados do Serviço C

⏱️ Tempo total: 2008ms

⚠️ Atenção: Note a diferença: 6 segundos vs 2 segundos. No código assíncrono, as três operações de I/O iniciaram quase ao mesmo tempo. Enquanto cada uma aguardava, a thread estava livre. Isso não é paralelismo — é uma única thread gerenciando múltiplas operações de I/O concorrentemente. Task.Delay libera a thread; Thread.Sleep bloqueia.

O que Acontece por Baixo dos Panos?

Quando você escreve await Task.Delay(2000), o compilador C# transforma seu método async em uma máquina de estados (state machine). Simplificando:

  1. O código antes do await executa normalmente.
  2. No ponto do await, se a Task ainda não completou, a thread é devolvida ao ThreadPool.
  3. Quando a operação de I/O termina, uma thread do pool é designada para continuar a execução a partir do ponto onde parou.
  4. Isso repete para cada await no método.

É por isso que async/await é tão poderoso em APIs web: enquanto uma requisição espera a resposta do banco de dados, a thread pode atender outras requisições.


CancellationToken: Por Que e Como Usar

O CancellationToken é um mecanismo do .NET para sinalizar o cancelamento cooperativo de operações assíncronas. Sem ele, se um usuário fecha o navegador, desconecta ou a requisição atinge timeout, sua API continua processando desnecessariamente — consumindo recursos do servidor sem ninguém para receber a resposta.

Por que é Importante?

Considere os cenários:

  1. O usuário faz uma requisição que demora 30 segundos e fecha a aba do navegador após 5 segundos.
  2. Um timeout é atingido no gateway ou load balancer.
  3. Sua API consulta um serviço externo que está lento e você quer impor um limite de tempo.

Sem CancellationToken, seu código continua executando a operação completa, desperdiçando CPU, memória e conexões de banco — recursos que poderiam atender outros usuários.

Exemplo Básico de CancellationToken

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Demonstração básica de CancellationToken
using var cts = new CancellationTokenSource();

// Cancela automaticamente após 3 segundos
cts.CancelAfter(TimeSpan.FromSeconds(3));

try
{
    Console.WriteLine("Iniciando operação demorada...");
    await OperacaoDemoradaAsync(cts.Token);
    Console.WriteLine("Operação concluída com sucesso!");
}
catch (OperationCanceledException)
{
    Console.WriteLine("⛔ Operação cancelada! Recursos liberados.");
}

static async Task OperacaoDemoradaAsync(CancellationToken cancellationToken)
{
    for (int i = 1; i <= 10; i++)
    {
        // Verifica se o cancelamento foi solicitado
        cancellationToken.ThrowIfCancellationRequested();

        Console.WriteLine($"  Processando etapa {i}/10...");
        await Task.Delay(1000, cancellationToken);
    }
}

Output esperado:

1
2
3
4
5
Iniciando operação demorada...
  Processando etapa 1/10...
  Processando etapa 2/10...
  Processando etapa 3/10...
⛔ Operação cancelada! Recursos liberados.

CancellationToken em Métodos de Serviço

Sempre propague o CancellationToken por toda a cadeia de chamadas:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// Interface do serviço
public interface IProdutoService
{
    Task<List<Produto>> ObterTodosAsync(CancellationToken cancellationToken = default);
    Task<Produto?> ObterPorIdAsync(int id, CancellationToken cancellationToken = default);
}

// Implementação do serviço
public class ProdutoService : IProdutoService
{
    private readonly HttpClient _httpClient;
    private readonly AppDbContext _dbContext;

    public ProdutoService(HttpClient httpClient, AppDbContext dbContext)
    {
        _httpClient = httpClient;
        _dbContext = dbContext;
    }

    public async Task<List<Produto>> ObterTodosAsync(
        CancellationToken cancellationToken = default)
    {
        // O Entity Framework aceita CancellationToken nativamente
        return await _dbContext.Produtos
            .AsNoTracking()
            .ToListAsync(cancellationToken);
    }

    public async Task<Produto?> ObterPorIdAsync(
        int id,
        CancellationToken cancellationToken = default)
    {
        // HttpClient também aceita CancellationToken
        var response = await _httpClient.GetAsync(
            $"/api/produtos/{id}",
            cancellationToken);

        response.EnsureSuccessStatusCode();

        return await response.Content
            .ReadFromJsonAsync<Produto>(cancellationToken: cancellationToken);
    }
}

💡 Dica: Use CancellationToken cancellationToken = default como último parâmetro. O default para CancellationToken é CancellationToken.None, o que significa “sem cancelamento” — mantendo a compatibilidade com código que não precisa de cancelamento.


CancellationToken na Controller do ASP.NET Core

Usar CancellationToken na controller é uma das melhores práticas para APIs ASP.NET Core, e faz total sentido: o framework já fornece um token de cancelamento automaticamente quando a requisição é abortada pelo cliente.

Como Funciona?

Quando você adiciona CancellationToken como parâmetro de uma action, o ASP.NET Core injeta automaticamente um token vinculado à conexão HTTP. Se o cliente desconectar (fechar aba, timeout, cancelar fetch), o token é sinalizado como cancelado.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/[controller]")]
public class ProdutosController : ControllerBase
{
    private readonly IProdutoService _produtoService;

    public ProdutosController(IProdutoService produtoService)
    {
        _produtoService = produtoService;
    }

    // ✅ CancellationToken injetado automaticamente pelo ASP.NET Core
    [HttpGet]
    public async Task<ActionResult<List<Produto>>> ObterTodos(
        CancellationToken cancellationToken)
    {
        var produtos = await _produtoService
            .ObterTodosAsync(cancellationToken);

        return Ok(produtos);
    }

    [HttpGet("{id:int}")]
    public async Task<ActionResult<Produto>> ObterPorId(
        int id,
        CancellationToken cancellationToken)
    {
        var produto = await _produtoService
            .ObterPorIdAsync(id, cancellationToken);

        if (produto is null)
            return NotFound();

        return Ok(produto);
    }

    // Endpoint com operação demorada — perfeito para CancellationToken
    [HttpPost("relatorio")]
    public async Task<ActionResult<Relatorio>> GerarRelatorio(
        [FromBody] RelatorioRequest request,
        CancellationToken cancellationToken)
    {
        try
        {
            var relatorio = await _produtoService
                .GerarRelatorioAsync(request, cancellationToken);

            return Ok(relatorio);
        }
        catch (OperationCanceledException)
        {
            // Log opcional — o ASP.NET Core já trata isso de forma limpa
            return StatusCode(499, "Requisição cancelada pelo cliente");
        }
    }
}

Cadeia Completa: Controller → Service → Repository

O verdadeiro poder do CancellationToken aparece quando ele percorre toda a cadeia de chamadas:

1
2
Cliente HTTP → Controller → Service → Repository → Banco de Dados
                  ↓ cancellationToken passado em toda a cadeia ↓
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Repository
public class ProdutoRepository
{
    private readonly AppDbContext _dbContext;

    public ProdutoRepository(AppDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public async Task<List<Produto>> ObterComFiltroAsync(
        string? categoria,
        CancellationToken cancellationToken = default)
    {
        var query = _dbContext.Produtos.AsNoTracking();

        if (!string.IsNullOrEmpty(categoria))
            query = query.Where(p => p.Categoria == categoria);

        // Se o cliente desconectar, a query no banco é cancelada
        return await query.ToListAsync(cancellationToken);
    }
}

⚠️ Atenção: Quando uma query ao banco de dados é cancelada via CancellationToken, o Entity Framework envia um comando de cancelamento ao banco. Isso libera a conexão e os recursos no servidor de banco de dados, não apenas na aplicação. É um ganho duplo: libera recursos tanto na API quanto no banco.


Exemplo Completo: Testando Síncrono vs Assíncrono

Para que você possa executar e ver a diferença na prática, aqui está um exemplo completo com uma API mínima que demonstra ambos os cenários:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// Program.cs — API Minimal demonstrando Síncrono vs Assíncrono
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// Endpoint SÍNCRONO — bloqueia a thread durante a "consulta"
app.MapGet("/api/sincrono", () =>
{
    var sw = System.Diagnostics.Stopwatch.StartNew();

    // Simula 3 consultas sequenciais de 1 segundo cada
    Thread.Sleep(1000); // consulta 1
    Thread.Sleep(1000); // consulta 2
    Thread.Sleep(1000); // consulta 3

    sw.Stop();
    return Results.Ok(new
    {
        modo = "síncrono",
        tempoMs = sw.ElapsedMilliseconds,
        mensagem = "Thread ficou bloqueada durante toda a execução"
    });
});

// Endpoint ASSÍNCRONO — libera a thread durante a "consulta"
app.MapGet("/api/assincrono", async (CancellationToken cancellationToken) =>
{
    var sw = System.Diagnostics.Stopwatch.StartNew();

    // Inicia 3 consultas assíncronas ao mesmo tempo
    var t1 = Task.Delay(1000, cancellationToken);
    var t2 = Task.Delay(1000, cancellationToken);
    var t3 = Task.Delay(1000, cancellationToken);

    await Task.WhenAll(t1, t2, t3);

    sw.Stop();
    return Results.Ok(new
    {
        modo = "assíncrono",
        tempoMs = sw.ElapsedMilliseconds,
        mensagem = "Thread foi liberada durante as esperas"
    });
});

// Endpoint que demonstra CancellationToken
app.MapGet("/api/demorado", async (CancellationToken cancellationToken) =>
{
    var etapas = new List<string>();

    try
    {
        for (int i = 1; i <= 10; i++)
        {
            cancellationToken.ThrowIfCancellationRequested();
            etapas.Add($"Etapa {i} concluída");
            await Task.Delay(1000, cancellationToken);
        }

        return Results.Ok(new
        {
            status = "concluído",
            etapas
        });
    }
    catch (OperationCanceledException)
    {
        return Results.Json(new
        {
            status = "cancelado",
            etapasCompletadas = etapas,
            mensagem = "Cliente desconectou. Recursos liberados."
        }, statusCode: 499);
    }
});

app.Run();

Para testar:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Terminal 1: Iniciar a API
dotnet run

# Terminal 2: Testar o endpoint síncrono
curl http://localhost:5000/api/sincrono
# Resposta: { "modo": "síncrono", "tempoMs": 3005, ... }

# Terminal 2: Testar o endpoint assíncrono
curl http://localhost:5000/api/assincrono
# Resposta: { "modo": "assíncrono", "tempoMs": 1003, ... }

# Terminal 2: Testar CancellationToken (cancele com Ctrl+C após 3s)
curl http://localhost:5000/api/demorado
# Se cancelar após 3s: { "status": "cancelado", "etapasCompletadas": [...] }

📝 Exemplo: Compare os tempos: o endpoint síncrono leva ~3 segundos (3 × 1s sequencial), enquanto o assíncrono leva ~1 segundo (3 × 1s concorrente). A mesma lógica se aplica a consultas reais de banco de dados, chamadas HTTP e leitura de arquivos.


Dicas e Boas Práticas

Aqui estão as práticas essenciais para escrever código assíncrono correto e performático em C#:

  • Sempre use async/await para operações de I/O. Chamadas HTTP, consultas de banco, leitura/escrita de arquivos — todas essas operações devem ser assíncronas. Nunca use .Result ou .Wait() em código assíncrono, pois isso pode causar deadlocks.

  • Prefira await Task.WhenAll() para operações independentes. Se você precisa consultar três serviços diferentes que não dependem um do outro, inicie todas as tasks e use Task.WhenAll para aguardar. Isso pode reduzir o tempo total de resposta drasticamente.

  • Sempre propague o CancellationToken. Recebeu um CancellationToken? Passe-o para todos os métodos assíncronos na cadeia. Entity Framework, HttpClient, Task.Delay — todos aceitam CancellationToken.

  • Use ConfigureAwait(false) em bibliotecas. Se você está escrevendo uma library (não uma aplicação), use await task.ConfigureAwait(false) para evitar capturar o SynchronizationContext desnecessariamente. Em APIs ASP.NET Core isso não é necessário porque não há SynchronizationContext.

  • Nunca use async void. A única exceção são event handlers. Métodos async void não podem ser aguardados e exceções não tratadas podem derrubar a aplicação. Sempre retorne Task ou Task<T>.

  • Não use Task.Run para encapsular código síncrono em APIs web. Isso não torna seu código verdadeiramente assíncrono — apenas move o bloqueio para outra thread do pool. Em APIs web, isso pode piorar a performance porque consome uma thread extra.

  • Nomeie métodos assíncronos com o sufixo Async. É uma convenção do .NET: ObterTodosAsync, SalvarAsync, EnviarEmailAsync. Isso torna claro que o método retorna uma Task e deve ser aguardado.


Erros Comuns (e Como Evitar)

❌ Usar .Result ou .Wait() (pode causar deadlock)

1
2
3
4
5
// ❌ ERRADO — pode causar deadlock em contextos com SynchronizationContext
var dados = ObterDadosAsync().Result;

// ✅ CORRETO — use await
var dados = await ObterDadosAsync();

❌ Não propagar CancellationToken

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// ❌ ERRADO — ignora o cancelamento do cliente
public async Task<List<Produto>> ObterTodosAsync(CancellationToken ct)
{
    return await _dbContext.Produtos.ToListAsync(); // Token não propagado!
}

// ✅ CORRETO — propaga o token
public async Task<List<Produto>> ObterTodosAsync(CancellationToken ct)
{
    return await _dbContext.Produtos.ToListAsync(ct);
}

❌ Usar Thread.Sleep em código assíncrono

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// ❌ ERRADO — bloqueia a thread do ThreadPool
public async Task ProcessarAsync()
{
    Thread.Sleep(5000); // Bloqueia!
    await FazerAlgoAsync();
}

// ✅ CORRETO — libera a thread
public async Task ProcessarAsync()
{
    await Task.Delay(5000); // Não bloqueia
    await FazerAlgoAsync();
}

Conclusão

A programação assíncrona em C# com async/await é uma das ferramentas mais poderosas do .NET para construir aplicações escaláveis e responsivas. Como vimos ao longo deste artigo, assíncrono não é paralelismo — são conceitos complementares que resolvem problemas diferentes. O assíncrono brilha em operações de I/O, liberando threads para atender mais requisições com menos recursos.

Entender como threads funcionam e o que significa ser thread-safe é essencial para evitar bugs sutis e difíceis de reproduzir, como race conditions. E o CancellationToken é indispensável para aplicações profissionais: ele permite que sua API libere recursos imediatamente quando uma requisição é cancelada, melhorando a escalabilidade e a experiência do usuário.

Na prática, lembre-se: use async/await para toda operação de I/O, propague o CancellationToken por toda a cadeia de chamadas (da controller ao banco de dados), evite .Result e .Wait(), e nunca confunda Thread.Sleep com Task.Delay. Se você seguir essas regras, seu código C# será mais eficiente, mais resiliente e mais profissional.

Se você gostou deste artigo e quer continuar aprimorando suas habilidades, explore os outros conteúdos do blog — desde segurança com BFF Backend For Frontend até automação de tarefas com Makefile.

💡 Dica final: Se a linguagem ou framework que você utiliza no dia a dia não oferece suporte nativo a programação assíncrona — ou implementa de forma complexa, verbosa e propensa a erros — vale a pena considerar seriamente adicionar ao seu portfólio uma linguagem madura que trate assincronicidade como cidadã de primeira classe. O C# é uma excelente opção: como vimos ao longo deste artigo, basta usar async, await e CancellationToken para escrever código assíncrono limpo, seguro e performático — sem callbacks aninhados, sem gerenciamento manual de threads e sem bibliotecas externas. O ecossistema .NET oferece suporte assíncrono nativo em praticamente tudo: Entity Framework, HttpClient, file I/O, streams, canais gRPC e até ASP.NET Core completo. Investir em uma linguagem que facilita a escrita de código assíncrono não é apenas uma questão de produtividade — é uma vantagem competitiva no mercado.


Leia Também


Referências