Operações sem resposta que não podem ser interrompidas são uma fonte comum de vazamentos de recursos, má experiência do usuário e falhas em cascata em sistemas distribuídos. CancellationToken é a primitiva de cancelamento cooperativo do .NET — ele não mata threads, sinaliza uma intenção, e seu código decide como reagir.
O Que CancellationToken Resolve
Antes dos tokens de cancelamento, parar uma operação async exigia flags bool compartilhadas, eventos personalizados ou thread-abort (que é quebrado por design). O problema: os chamadores não tinham uma forma padrão de dizer a uma operação "pare o que está fazendo".
CancellationToken estabelece um contrato:
- Produtor (
CancellationTokenSource) decide quando cancelar - Consumidor (seu método async) verifica o token e reage apropriadamente
- Framework (ASP.NET Core, EF Core,
HttpClient) faz isso automaticamente quando você passa o token
// Sem cancelamento — o chamador não tem como parar isto; a única rede de
// segurança é o Timeout global do HttpClient, 100 segundos por padrão
public async Task<string> FetchDataAsync(string url)
{
var response = await _httpClient.GetAsync(url);
return await response.Content.ReadAsStringAsync();
}
// Com cancelamento — respeita a intenção do chamador
public async Task<string> FetchDataAsync(string url, CancellationToken cancellationToken)
{
var response = await _httpClient.GetAsync(url, cancellationToken);
return await response.Content.ReadAsStringAsync(cancellationToken);
}O insight principal: o cancelamento é cooperativo. O token não pode forçar seu código a parar. Você deve observá-lo.
Cada Afirmação Aqui É Verificada, Não Narrada
O cancelamento cooperativo tem uma propriedade útil: cada afirmação sobre ele é
verificável como um fato concreto — o loop parou?, em que estado a tarefa terminou?, que
tipo de exceção saiu?, de quem é o token que ela carrega?
samples/cancellationtoken-csharp
transforma as afirmações deste artigo em asserções, sem corridas de timing: cada token é
cancelado antes do trabalho que deveria parar, ou o trabalho espera infinitamente de
modo que só o cancelamento pode encerrá-lo.
| Demo | Verifica |
|---|---|
| Cooperativo | um loop que nunca lê o token processa 5/5 itens depois de Cancel(); a versão que o observa processa 0/5 e sua tarefa termina Canceled, não Faulted |
| Drenagem graciosa | um loop while (!token.IsCancellationRequested) termina naturalmente — o chamador vê RanToCompletion, sem exceção |
| Tokens vinculados | o filtro when (callerToken.IsCancellationRequested) separa "meu timeout" de "o chamador cancelou" nas duas direções |
Register | o callback dispara exatamente uma vez com duas chamadas a Cancel(); um registro descartado nunca dispara; registrar em um token já cancelado executa o callback sincronamente |
Task.Run | com um token pré-cancelado o delegate nunca executa; await lança TaskCanceledException carregando o token do chamador |
HttpClient | Timeout → TaskCanceledException com TimeoutException interna; um token do chamador → sem TimeoutException interna, e a exceção carrega o token do chamador |
| Descarte | Cancel() depois de Dispose() lança ObjectDisposedException |

O demo do HttpClient merece uma nota: para provar o comportamento do timeout sem
instabilidade de rede, o sample inicia um TcpListener em uma porta de loopback que
aceita a conexão e nunca responde — uma forma determinística de fazer uma requisição
HTTP ficar pendurada.
CancellationTokenSource — Criando Tokens
CancellationTokenSource (CTS) é o controlador. Ele cria o token e mantém a capacidade de cancelá-lo.
// Cancelamento manual básico
var cts = new CancellationTokenSource();
CancellationToken token = cts.Token;
// Passar token para o trabalho
var workTask = DoWorkAsync(token);
// Cancelar de outra thread / ação do usuário
cts.Cancel(); // sinaliza o cancelamento; os callbacks registrados rodam NESTA thread, agora
// cts.CancelAsync(); // .NET 8+ — retorna um Task em vez de executar os callbacks inline
await workTask; // lança OperationCanceledExceptionCiclo de Vida e Descarte
// CancellationTokenSource implementa IDisposable
// Sempre descartar quando você é o proprietário do source
using var cts = new CancellationTokenSource();
// Ou em um try/finally se não usar 'using'
var cts = new CancellationTokenSource();
try
{
await DoWorkAsync(cts.Token);
}
finally
{
cts.Dispose(); // libera o WaitHandle interno se alocado
}Uma correção de uma versão anterior deste artigo: ele mostrava
await using var cts = new CancellationTokenSource() com a afirmação de que o CTS ganhou
IAsyncDisposable no .NET 6. Não ganhou — essa linha não compila, o que descobri da
forma direta ao portar os snippets do artigo para o projeto de sample.
CancellationTokenSource é IDisposable comum; use using.
Chamar Cancel() após Dispose() lança ObjectDisposedException — o demo 7 do sample verifica exatamente isso. Se múltiplos componentes compartilham um CTS, coordene cuidadosamente a propriedade.
Passando Tokens Pela Cadeia de Chamadas
Esta é a prática mais importante. Todo método async que realiza I/O, aguarda ou itera em um loop deve aceitar e encaminhar um CancellationToken.
// Camada de repositório
public async Task<Order> GetOrderAsync(int orderId, CancellationToken cancellationToken = default)
{
return await _dbContext.Orders
.Include(o => o.Items)
.FirstOrDefaultAsync(o => o.Id == orderId, cancellationToken);
}
// Camada de serviço — passa o token adiante
public async Task<OrderDto> ProcessOrderAsync(int orderId, CancellationToken cancellationToken = default)
{
var order = await _orderRepo.GetOrderAsync(orderId, cancellationToken);
// Verificar antes de operação custosa
cancellationToken.ThrowIfCancellationRequested();
var enriched = await _enrichmentService.EnrichAsync(order, cancellationToken);
return _mapper.Map<OrderDto>(enriched);
}
// Controlador ASP.NET Core — o framework passa cancellationToken automaticamente
[HttpGet("{id}")]
public async Task<IActionResult> GetOrder(int id, CancellationToken cancellationToken)
{
var order = await _orderService.ProcessOrderAsync(id, cancellationToken);
return order is null ? NotFound() : Ok(order);
}O parâmetro = default permite que chamadores que não se importam com o cancelamento o omitam — CancellationToken.None é o padrão, que nunca dispara.
A Convenção = default
// Estas são equivalentes — CancellationToken.None nunca dispara
await DoWorkAsync();
await DoWorkAsync(CancellationToken.None);
await DoWorkAsync(default);
await DoWorkAsync(default(CancellationToken));Tokens com Timeout — CancelAfter
O caso de uso mais comum: cancelar uma operação se demorar muito.
// CancelAfter arma um timer sobre o source existente (disponível desde o .NET Framework 4.5)
var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(30));
try
{
var result = await FetchDataAsync(cts.Token);
}
catch (OperationCanceledException) when (cts.IsCancellationRequested)
{
// Distingue timeout de cancelamento do usuário
throw new TimeoutException("Operação excedeu 30s");
}// Sobrecarga do construtor — define timeout na criação
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
try
{
await ProcessAsync(cts.Token);
}
catch (OperationCanceledException)
{
_logger.LogWarning("Operação excedeu o tempo limite de 30 segundos");
throw;
}Adicionando um Timeout a um Token Existente
// CreateLinkedTokenSource + CancelAfter: cancela quando o token upstream dispara
// OU quando o timeout expira, o que acontecer primeiro
using var cts = CancellationTokenSource.CreateLinkedTokenSource(requestToken);
cts.CancelAfter(TimeSpan.FromSeconds(5));(Uma versão anterior deste artigo chamava isto de "TimeoutToken do .NET 8" — essa API
não existe. As duas peças são antigas: CreateLinkedTokenSource chegou com a TPL no
.NET Framework 4, CancelAfter no 4.5.)
Prefira CancelAfter a criar um novo CTS com timeout no construtor quando você já tem um CTS — ele reutiliza a instância existente em vez de alocar uma nova.
IsCancellationRequested vs ThrowIfCancellationRequested
Duas formas de verificar o cancelamento — cada uma tem seu lugar.
public async Task ProcessItemsAsync(IEnumerable<Item> items, CancellationToken cancellationToken)
{
foreach (var item in items)
{
// Opção 1: ThrowIfCancellationRequested — lança OperationCanceledException
// Usar dentro de loops ou entre etapas onde você quer abortar imediatamente
cancellationToken.ThrowIfCancellationRequested();
await ProcessItemAsync(item, cancellationToken);
}
}public async Task ProcessWithCleanupAsync(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
// Opção 2: IsCancellationRequested — verificação booleana, sem exceção
// Usar quando você quer fazer limpeza antes de retornar
var batch = await ReadBatchAsync(cancellationToken);
if (batch.Count == 0)
break;
await ProcessBatchAsync(batch, cancellationToken);
}
// Termina naturalmente — o chamador vê uma tarefa concluída, não cancelada
// Apropriado para cenários de desligamento gracioso
}A diferença entre os dois é visível em Task.Status, e o sample verifica os dois lados:
a versão com ThrowIfCancellationRequested termina em Canceled (a máquina de estados
async trata OperationCanceledException como caso especial — a tarefa não fica
Faulted), enquanto a drenagem com verificação booleana termina em RanToCompletion
como se nada tivesse acontecido. Escolha conforme qual dessas duas histórias você quer
que o chamador veja.
Quando Usar Cada Um
| Cenário | Recomendado |
|---|---|
| Corpo do loop entre iterações | ThrowIfCancellationRequested() |
| Condição do loop worker | !IsCancellationRequested |
| Antes de trabalho CPU custoso | ThrowIfCancellationRequested() |
| Drenagem graciosa / limpeza | IsCancellationRequested |
| Passar para APIs awaitable | Passar o token diretamente |
| Após um awaitable concluir | ThrowIfCancellationRequested() opcional |
// Prático: verificar antes de trabalho custoso, passar através de awaits
public async Task<byte[]> CompressAndUploadAsync(
Stream data,
CancellationToken cancellationToken)
{
// Verificar antes de trabalho intensivo em CPU
cancellationToken.ThrowIfCancellationRequested();
var compressed = await CompressAsync(data, cancellationToken);
// Não é necessário verificar novamente — CompressAsync já fez isso internamente
// Mas se houver uma lacuna entre awaits sem verificação interna:
cancellationToken.ThrowIfCancellationRequested();
return await _storage.UploadAsync(compressed, cancellationToken);
}Tokens Vinculados — CreateLinkedTokenSource
Sistemas reais frequentemente têm múltiplas fontes de cancelamento: timeout da requisição, cancelamento do usuário, desligamento do servidor. CreateLinkedTokenSource os combina em um único token.
public async Task<SearchResult> SearchAsync(
string query,
CancellationToken requestCancellationToken) // da requisição HTTP
{
// Impor um timeout adicional por operação
// O token vinculado dispara se QUALQUER fonte disparar primeiro
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
requestCancellationToken,
timeoutCts.Token);
try
{
return await _searchEngine.SearchAsync(query, linkedCts.Token);
}
catch (OperationCanceledException) when (!requestCancellationToken.IsCancellationRequested)
{
// A requisição não foi cancelada — deve ser nosso timeout
throw new TimeoutException($"Busca excedeu 5s para a consulta: {query}");
}
// Se requestCancellationToken disparou, OperationCanceledException se propaga naturalmente
}Essa classificação é fácil de inverter sutilmente, então o demo 3 do sample verifica os
dois ramos: com um token do chamador limpo e um timeout disparado ele classifica
"timeout", e com um chamador cancelado classifica "chamador" — os mesmos filtros catch
acima.
Múltiplas Fontes Upstream
// Combinar três fontes: requisição + cancelamento do usuário + desligamento global
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
httpContext.RequestAborted, // requisição HTTP cancelada
userCancellationToken, // ação explícita do usuário
_appLifetime.ApplicationStopping); // servidor encerrando
await DoLongOperationAsync(linkedCts.Token);CreateLinkedTokenSource aloca um novo CancellationTokenSource. Sempre descarte-o, especialmente em caminhos de alto throughput como endpoints API frequentes.
Register() — Callbacks de Limpeza
Register() anexa um callback que dispara quando o cancelamento é solicitado. Útil para cancelar operações não canceláveis ou liberar recursos.
public async Task<string> PollWithCallbackAsync(
Func<Task<string?>> pollFunc,
CancellationToken cancellationToken)
{
var tcs = new TaskCompletionSource<string>(
TaskCreationOptions.RunContinuationsAsynchronously);
// Registrar limpeza: cancelar o TaskCompletionSource quando o token disparar
using var registration = cancellationToken.Register(() =>
{
tcs.TrySetCanceled(cancellationToken);
});
// Iniciar polling em background
_ = Task.Run(async () =>
{
while (!tcs.Task.IsCompleted)
{
var result = await pollFunc();
if (result is not null)
{
tcs.TrySetResult(result);
return;
}
await Task.Delay(500); // intervalo de polling
}
});
return await tcs.Task;
}// Conectar API legada baseada em callbacks ao cancelamento
public Task WaitForEventAsync(LegacyEventSource source, CancellationToken cancellationToken)
{
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
void OnEvent(object? sender, EventArgs e) => tcs.TrySetResult();
source.EventOccurred += OnEvent;
// Limpar inscrição se cancelado
cancellationToken.Register(() =>
{
source.EventOccurred -= OnEvent;
tcs.TrySetCanceled(cancellationToken);
});
return tcs.Task;
}Register() Retorna um Descartável
// O CancellationTokenRegistration deve ser descartado quando não for mais necessário
// para evitar manter callbacks vivos por mais tempo do que o necessário
using var registration = cancellationToken.Register(() => DoCleanup());
// Sem 'using', o callback vive até que o SOURCE seja descartado
// Geralmente está ok para operações de curta duração, mas pode vazar em operações longasTrês comportamentos de Register que vale a pena saber de cor, todos verificados pelo
demo 4 do sample: o callback dispara exatamente uma vez mesmo que Cancel() seja chamado
duas vezes; um registro descartado nunca dispara; e registrar em um token já cancelado
executa o callback sincronamente, na sua thread atual, dentro da própria chamada a
Register — o que significa que um callback que pega um lock pode causar um deadlock
ali mesmo.
Desligamento Gracioso no ASP.NET Core
O ASP.NET Core expõe eventos do ciclo de vida da aplicação através de IHostApplicationLifetime. Estes são instâncias de CancellationToken pré-configuradas.
// Program.cs — configurar timeout de desligamento
builder.Services.Configure<HostOptions>(options =>
{
// Dar 30s para os serviços em background terminarem antes de forçar o encerramento
options.ShutdownTimeout = TimeSpan.FromSeconds(30);
});// Serviço em background — desligamento gracioso adequado
public class OrderProcessingService : BackgroundService
{
private readonly ILogger<OrderProcessingService> _logger;
private readonly IOrderQueue _queue;
public OrderProcessingService(ILogger<OrderProcessingService> logger, IOrderQueue queue)
{
_logger = logger;
_queue = queue;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Processamento de pedidos iniciado");
// stoppingToken é fornecido pelo host — dispara no SIGTERM/Ctrl+C
await foreach (var order in _queue.ReadAllAsync(stoppingToken))
{
try
{
await ProcessOrderAsync(order, stoppingToken);
}
catch (OperationCanceledException)
{
// Não registrar como erro — isso é esperado no desligamento
_logger.LogInformation("Desligamento solicitado, parando processamento de pedidos");
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "Falha ao processar pedido {OrderId}", order.Id);
// Continuar processando o próximo pedido
}
}
_logger.LogInformation("Processamento de pedidos parado");
}
private async Task ProcessOrderAsync(Order order, CancellationToken cancellationToken)
{
// Todas as chamadas descendentes recebem o stoppingToken
await _orderService.ValidateAsync(order, cancellationToken);
await _orderService.FulfillAsync(order, cancellationToken);
await _notificationService.SendConfirmationAsync(order, cancellationToken);
}
}IHostApplicationLifetime para Código que Não É BackgroundService
public class DataSyncService
{
private readonly IHostApplicationLifetime _lifetime;
public DataSyncService(IHostApplicationLifetime lifetime)
{
_lifetime = lifetime;
}
public async Task SyncAsync(CancellationToken userCancellationToken)
{
// Combinar cancelamento de requisição do usuário com desligamento da aplicação
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
userCancellationToken,
_lifetime.ApplicationStopping);
await PerformSyncAsync(linkedCts.Token);
}
}Cancelamento de Requisições em Controllers
// ASP.NET Core vincula automaticamente CancellationToken a HttpContext.RequestAborted
[ApiController]
[Route("api/[controller]")]
public class ReportsController : ControllerBase
{
[HttpGet("{id}/generate")]
public async Task<IActionResult> GenerateReport(
int id,
CancellationToken cancellationToken) // vinculado automaticamente de HttpContext.RequestAborted
{
try
{
var report = await _reportService.GenerateAsync(id, cancellationToken);
return Ok(report);
}
catch (OperationCanceledException)
{
// Cliente desconectou — ninguém jamais lerá esta resposta.
// 499 é uma convenção do nginx, NÃO algo que o ASP.NET Core produza ou
// trate por você; retorná-lo aqui apenas rotula a requisição nos seus
// próprios logs e métricas em vez de um 200 ou 500 enganoso.
return StatusCode(499);
}
}
}Cancelando Requisições HttpClient
HttpClient aceita CancellationToken em todos os métodos de requisição. Passe-o sempre.
public class WeatherService
{
private readonly HttpClient _httpClient;
public WeatherService(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<WeatherData> GetWeatherAsync(
string city,
CancellationToken cancellationToken)
{
// Cancelar se o chamador cancelar OU se a requisição demorar > 10s
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
cancellationToken,
timeoutCts.Token);
try
{
var response = await _httpClient.GetAsync(
$"/weather/{city}",
linkedCts.Token);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<WeatherData>(linkedCts.Token)
?? throw new InvalidOperationException("Resposta vazia");
}
catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested)
{
throw new TimeoutException($"Requisição de clima para {city} excedeu o tempo limite");
}
}
}Timeout Global do HttpClient vs Cancelamento por Requisição
// HttpClient.Timeout — aplica a TODAS as requisições daquela instância do cliente
// CancellationToken — por requisição
// AMBOS emergem como TaskCanceledException (que deriva de OperationCanceledException).
// Desde o .NET 5 o timeout carrega uma TimeoutException interna — essa é a verificação
// que a Microsoft adicionou justamente porque antes os dois casos eram indistinguíveis:
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
// HttpClient.Timeout disparou
}
catch (TaskCanceledException) when (cancellationToken.IsCancellationRequested)
{
// Chamador cancelou
}O demo 6 do sample verifica os dois ramos contra um servidor local que aceita a conexão e
nunca responde: o caso do Timeout tem a TimeoutException interna, o caso do token do
chamador não — e no caso do chamador ex.CancellationToken é igual ao token do chamador
(verifiquei no .NET 10; também vale desde o .NET 5, quando o token passou a ser propagado
para a exceção).
Cancelamento de Consultas EF Core
O Entity Framework Core passa o CancellationToken para o driver do banco de dados. A consulta é cancelada no nível do banco de dados — sem desperdiçar recursos de BD.
public class ProductRepository
{
private readonly AppDbContext _context;
public ProductRepository(AppDbContext context)
{
_context = context;
}
// Passar token para todos os métodos async do EF Core
public async Task<List<Product>> GetActiveProductsAsync(
string category,
CancellationToken cancellationToken = default)
{
return await _context.Products
.Where(p => p.Category == category && p.IsActive)
.OrderBy(p => p.Name)
.AsNoTracking()
.ToListAsync(cancellationToken); // cancela a consulta no BD
}
public async Task<int> BulkUpdatePricesAsync(
string category,
decimal multiplier,
CancellationToken cancellationToken = default)
{
// ExecuteUpdateAsync cancela se o token disparar durante a operação
return await _context.Products
.Where(p => p.Category == category)
.ExecuteUpdateAsync(
setters => setters.SetProperty(p => p.Price, p => p.Price * multiplier),
cancellationToken);
}
public async Task<Product?> FindWithRetryAsync(
int id,
CancellationToken cancellationToken = default)
{
for (int attempt = 0; attempt < 3; attempt++)
{
try
{
return await _context.Products.FindAsync(
new object[] { id },
cancellationToken);
}
catch (OperationCanceledException)
{
throw; // nunca suprimir o cancelamento
}
catch (Exception ex) when (attempt < 2)
{
_logger.LogWarning(ex, "Tentativa {Attempt} falhou, tentando novamente", attempt + 1);
await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt)), cancellationToken);
}
}
return null;
}
}Combinando Timeout de Operação + Cancelamento de Requisição
Um padrão completo para uso em produção: timeout por operação combinado com o cancelamento da requisição upstream.
public class ProductSearchService
{
private readonly ISearchIndex _searchIndex;
private readonly ILogger<ProductSearchService> _logger;
// Timeout configurável por tipo de operação
private static readonly TimeSpan SearchTimeout = TimeSpan.FromSeconds(3);
private static readonly TimeSpan SuggestTimeout = TimeSpan.FromMilliseconds(500);
public ProductSearchService(ISearchIndex searchIndex, ILogger<ProductSearchService> logger)
{
_searchIndex = searchIndex;
_logger = logger;
}
public async Task<SearchResult> SearchAsync(
SearchRequest request,
CancellationToken requestCancellationToken = default)
{
using var cts = CancellationTokenSource.CreateLinkedTokenSource(requestCancellationToken);
cts.CancelAfter(SearchTimeout);
try
{
return await _searchIndex.SearchAsync(request, cts.Token);
}
catch (OperationCanceledException) when (requestCancellationToken.IsCancellationRequested)
{
_logger.LogInformation("Busca cancelada pela requisição para a consulta: {Query}", request.Query);
throw; // relançar como está, propagar upstream
}
catch (OperationCanceledException)
{
// Nosso timeout disparou, não o cancelamento da requisição
_logger.LogWarning("Busca excedeu o tempo limite após {Timeout}s para a consulta: {Query}",
SearchTimeout.TotalSeconds, request.Query);
throw new SearchTimeoutException(request.Query, SearchTimeout);
}
}
public async Task<IReadOnlyList<string>> GetSuggestionsAsync(
string prefix,
CancellationToken requestCancellationToken = default)
{
using var cts = CancellationTokenSource.CreateLinkedTokenSource(requestCancellationToken);
cts.CancelAfter(SuggestTimeout);
try
{
return await _searchIndex.GetSuggestionsAsync(prefix, cts.Token);
}
catch (OperationCanceledException)
{
// Sugestões não são críticas — retornar vazio em timeout ou cancelamento
_logger.LogDebug("Sugestões excederam o tempo ou foram canceladas para o prefixo: {Prefix}", prefix);
return Array.Empty<string>();
}
}
}Erros Comuns
Nunca Suprimir OperationCanceledException
// ERRADO — esconde o cancelamento, o chamador acha que o trabalho foi concluído normalmente
try
{
await DoWorkAsync(cancellationToken);
}
catch (OperationCanceledException)
{
// Suprimido! O chamador não tem ideia de que o cancelamento ocorreu
}
// CORRETO — registrar se necessário, mas sempre relançar
catch (OperationCanceledException ex)
{
_logger.LogInformation("Trabalho foi cancelado");
throw; // preservar a exceção original e o stack trace
}Não Envolver Desnecessariamente
// ERRADO — perde a semântica de cancelamento
catch (OperationCanceledException ex)
{
throw new ApplicationException("Operação falhou", ex); // ruim!
}
// CORRETO — envolver apenas se estiver adicionando contexto real, e usar um tipo
// que ainda comunique cancelamento ou usar a exceção original
catch (OperationCanceledException)
{
throw; // ou deixar propagar naturalmente
}Evitar Task.Run Sem Encaminhar o Token
// ERRADO — fire-and-forget ignora o cancelamento
var task = Task.Run(() => HeavyComputation()); // sem token!
// CORRETO — token passado para Task.Run E para o trabalho
var task = Task.Run(() => HeavyComputation(cancellationToken), cancellationToken);
// O token externo cancela o agendamento da tarefa; o interno cancela o trabalho em siO efeito do token externo é observável: o demo 5 do sample passa um token já cancelado
para Task.Run e verifica que o delegate nunca executa — a tarefa vai direto para
Canceled e await lança TaskCanceledException carregando esse token.
Não Usar CancellationToken para Controle de Fluxo
// ERRADO — CancellationToken é para cancelamento, não para ramificações
if (cancellationToken.IsCancellationRequested)
{
return GetCachedResult(); // usando cancelamento como "caminho rápido"!
}
// CORRETO — verificar cancelamento, então lançar ou retornar limpo
cancellationToken.ThrowIfCancellationRequested();
return await FetchFreshResult(cancellationToken);Referência de Decisão
Qual Padrão Usar
| Situação | Padrão |
|---|---|
| Timeout simples em uma operação | new CancellationTokenSource(timeout) |
| Adicionar timeout a token existente | CreateLinkedTokenSource + CancelAfter |
| Requisição HTTP cancelada pelo cliente | HttpContext.RequestAborted passado adiante |
| Desligamento de serviço em background | BackgroundService.stoppingToken |
| Múltiplas fontes de cancelamento | CreateLinkedTokenSource(token1, token2, ...) |
| Limpeza no cancelamento | cancellationToken.Register(callback) |
| Verificar em loop fechado | ThrowIfCancellationRequested() |
| Drenagem graciosa sem exceção | IsCancellationRequested como condição do loop |
Decisão de Tratamento de Exceções
| Cenário | Ação |
|---|---|
| Cancelamento esperado (cliente desconectado) | Capturar, registrar debug/info, retornar 499 ou vazio |
| Timeout é excepcional para esta operação | Capturar, envolver em exceção de domínio, registrar warning |
| Worker em background cancelado no desligamento | Capturar, registrar info, sair do loop limpo |
| Propagando por middleware/pipeline | Relançar (throw;) sem envolver |
| Loop de retry | Capturar outras exceções, relançar cancelamento |
Resumo
CancellationToken funciona bem quando você o trata de forma consistente: aceitá-lo em todo método async, encaminhá-lo para toda chamada awaitable, verificar entre etapas com ThrowIfCancellationRequested, e nunca suprimir OperationCanceledException. O padrão de token vinculado lida com a necessidade real de combinar múltiplas fontes de cancelamento — ciclo de vida da requisição, timeout por operação e desligamento da aplicação — em um único token que qualquer biblioteca descendente pode usar sem conhecer a origem.
A diferença entre um serviço resiliente e um que vaza recursos sob carga muitas vezes se resume a se o cancelamento está conectado de ponta a ponta.
Cada afirmação de comportamento acima é verificada por
samples/cancellationtoken-csharp
— clone-o e execute dotnet run se quiser ver alguma falhar em um runtime futuro.
O cancelamento se sobrepõe a dois temas vizinhos que valem a leitura em seguida.
OperationCanceledException é uma exceção como qualquer outra, então as regras de
tratamento de exceções em async — em especial em
torno de Task.WhenAll e trabalho fire-and-forget — se aplicam diretamente a operações
canceladas. E se você está transmitindo resultados em vez de retornar uma lista,
IAsyncEnumerable<T> tem seu próprio mecanismo de
cancelamento, que não se comporta como um método async comum.