A adoção do paradigma assíncrono exige uma revisão abrangente da cadeia de chamadas. Para extrair o máximo de desempenho, todos os invocadores na cadeia devem operar de forma assíncrona. Uma implementação parcial frequantemente resulta em um desempenho inferior ao de uma abordagem totalmente síncrona.
// Exemplo inadequado: Bloqueia a thread atual para aguardar o resultado.
public int ProcessarDados()
{
var valor = ObterDadosAsync().Result;
return valor + 10;
}
// Exemplo adequado: Utiliza 'await' para não bloquear a thread.
public async Task<int> ProcessarDadosAsync()
{
var valor = await ObterDadosAsync();
return valor + 10;
}
Evite 'async void'
Em aplicativos ASP.NET Core, o uso de métodos assíncronos que retornam 'void' (async void) é quase sempre uma prática problemática. Eles são frequentemente usados para operações "dispare e esqueça", mas qualquer exceção não tratada dentro deles pode resultar no encerramento abrupto de todo o processo da aplicação, uma vez que escapa ao mecanismo normal de propagação de falhas via Task.
// Exemplo problemático: Exceções não tratadas podem derrubar a aplicação.
public class ControladorDePedidos : Controller
{
[HttpPost("/processar")]
public IActionResult IniciarProcesso()
{
ExecutarOperacaoEmSegundoPlano();
return Accepted();
}
public async void ExecutarOperacaoEmSegundoPlano()
{
var dados = await ObterDadosExternosAsync();
Processar(dados);
}
}
// Abordagem recomendada: Retorna Task e captura exceções.
public class ControladorDePedidos : Controller
{
[HttpPost("/processar")]
public IActionResult IniciarProcesso()
{
Task.Factory.StartNew(ExecutarOperacaoEmSegundoPlano);
return Accepted();
}
public async Task ExecutarOperacaoEmSegundoPlano()
{
var dados = await ObterDadosExternosAsync();
Processar(dados);
}
}
// Registra um manipulador global para observar exceções em tasks não aguardadas.
TaskScheduler.UnobservedTaskException += (remetente, argumentos) =>
{
logger.LogError("Exceção não observada: {Excecoes}", argumentos.Exception.InnerExceptions);
argumentos.SetObserved();
};
Prefira 'Task.FromResult' ou 'ValueTask' para operações simples
Task.FromResult é ideal para envolver um valor já disponível em uma Task concluída, evitando a sobrecarga de iniciar um novo fluxo de execução na thread pool. Para operações síncronas e rápidas dentro de um método com assinatura assíncrona, utilize-a em vez de Task.Run.
// Ineficiente: Aloca um fluxo de execução da pool para uma operação trivial.
public Task<int> SomarAsync(int a, int b)
{
return Task.Run(() => a + b);
}
// Melhor: Cria uma Task já concluída. (Alocação de objeto Task)
public Task<int> SomarAsync(int a, int b)
{
return Task.FromResult(a + b);
}
// Ótimo: Evita a alocação de um objeto Task no heap.
public ValueTask<int> SomarAsync(int a, int b)
{
return new ValueTask<int>(a + b);
}
Não bloqueie a Thread Pool com operações de longa duração
Task.Run destina-se a operações curtas e não bloqueantes. Para trabalho que consome tempo significativo, ele pode esgotar os threads da pool, prejudicando a escalabilidade. Para tais cenários, utilize Task.Factory.StartNew com a opção TaskCreationOptions.LongRunning, que aconselha o escalonador a alocar um thread dedciado.
// Ruim: Pode saturar a Thread Pool.
var tarefa = Task.Run(() => RealizarCalculoDemorado());
// Melhor para operações de longa duração: Cria um thread dedicado.
var tarefa = Task.Factory.StartNew(() => RealizarCalculoDemorado(),
CancellationToken.None,
TaskCreationOptions.LongRunning,
TaskScheduler.Default);
Contudo, evite usar LongRunning em métodos assíncronos, pois o thread dedicado será liberado após o primeiro await.
Evite bloqueios com 'Task.Result' ou 'Task.Wait'
Chamar .Result ou .Wait() em uma Task bloqueia a thread de execução atual. Em contextos com SynchronizationContext (como UI ou ASP.NET tradicional), isso frequentemente leva a deadlocks. A solução universal é usar await.
// Todos os exemplos abaixo são problemáticos por bloquearem a thread.
public string ExecutarBloqueio1() => OperacaoAsync().Result;
public string ExecutarBloqueio2() => OperacaoAsync().GetAwaiter().GetResult();
public string ExecutarBloqueio3()
{
var tarefa = OperacaoAsync();
tarefa.Wait();
return tarefa.Result;
}
// A solução correta é converter o método para 'async' e usar 'await'.
public async Task<string> ExecutarAssincronoAsync() => await OperacaoAsync();
Gerenciamento correto de CancellationTokenSource
Ao usar CancellationTokenSource com um timeout ou para gerenciar o ciclo de vida de uma operação, garanta seu descarte adequado utilizando o bloco using. Isso libera os recursos de sistema associados de forma determinística.
using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)))
{
// Executa uma operação longa, verificando periodicamente o cancelamento.
while (!cts.Token.IsCancellationRequested)
{
// ... realizar um passo do trabalho ...
}
}
Sempre passe o CancellationToken para os métodos de API que o suportam. Isso permite que a operação seja cooperativamente cancelada de fora.
using (var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)))
using (var httpClient = new HttpClient())
{
// A chamada HTTP será cancelada após 1 minuto.
var resposta = await httpClient.GetAsync("https://api.dados.com/recurso", cts.Token);
}