Dominando o Comportamento do Task.ContinueWith no .NET

O método Task.ContinueWith é frequentemente fonte de confusão para desenvolvedores que esperam um fluxo de execução linear, especialmente quando comparado à simplicidade sintática do async/await. O principal desafio reside no fato de que o encadeamento de ContinueWith não garante, por si só, que as tarefas subsequentes esperem o término lógico da enterior, especialmente quando operações assíncronas (como Task.Delay) estão envolvidas.

Considere o seguinte cenário, onde as tarefas são disparadas em sequência, mas sem a devida espera pelo encerramento interno dos métodos assíncronos:

public async Task ExecutarFluxo()
{
   var tarefaPrincipal = Task.Run(() => RealizarTarefa("Processo A"))
       .ContinueWith(t => RealizarTarefa("Processo B"))
       .ContinueWith(t => RealizarTarefa("Processo C"));
}

public async Task RealizarTarefa(string nome)
{
   await Task.Delay(500);
   Console.WriteLine($"{nome} concluído em: {Environment.CurrentManagedThreadId}");
}

Neste exemplo, o uso direto de ContinueWith retorna um Task<Task>, o que pode causar comportamento inesperado, pois o encadeamento avalia a conclusão do invólucro da tarefa, não a conclusão da operação assíncrona interna.

Controlando o Fluxo com TaskContinuationOptions

Para gerenciar estados específicos de conclusão, podemos utilizar o enumerador TaskContinuationOptions. No entanto, é necessário cautela: definir opções como NotOnRanToCompletion fará com que o bloco subsequente seja ignorado caso a tarefa anterior tenha sido bem-sucedida, interrompendo toda a cadeia.

A Abordagem Correta: Unindo Unwrapping e Await

A melhor forma de garantir uma execução sequencial e legível, mantendo o controle sobre o fluxo, é transformar os métodos de retorno em Task e aguardar explicitamente a conclusão das tarefas, tratando o encadeamento de forma assíncrona:

public async Task ExecutarFluxoCorreto()
{
   // Inicia a primeira etapa
   var etapa1 = await Task.Run(() => RealizarTarefa("Etapa 1"));

   // Garante a execução sequencial com o auxílio do await
   await RealizarTarefa("Etapa 2");
   await RealizarTarefa("Etapa 3");
}

Se o uso de ContinueWith for mandatório por requisitos de arquitetura (como em fluxos reativos ou bibliotecas legadas), a prática recomendada é utilizar o método Unwrap(). Isso resolve o problema do "Task dentro de Task", permitindo que o await aguarde a conclusão da tarefa interna, garantindo que o código siga uma ordem lógica e previsível.

Tags: dotnet CSharp asynchronous Multithreading tasks

Publicado em 6-27 23:14