Guia de Programação Assíncrona em .NET

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);
}

Tags: async-await CSharp dotnet cancellationtokensource taskparallelibrary

Publicado em 6-30 20:10