Interrompendo uma tarefa de longa execução no TPL

Nosso aplicativo usa o TPL para serializar (potencialmente) unidades de trabalho de longa duração. A criação do trabalho (tarefas) é orientada pelo usuário e pode ser cancelada a qualquer momento. Para ter uma interface de usuário responsiva, se o trabalho atual não for mais necessário, gostaríamos de abandonar o que estávamos fazendo e iniciar imediatamente uma tarefa diferente.

As tarefas são enfileiradas da seguinte forma:

private Task workQueue;
private void DoWorkAsync
    (Action<WorkCompletedEventArgs> callback, CancellationToken token) 
{
   if (workQueue == null)
   {
      workQueue = Task.Factory.StartWork
          (() => DoWork(callback, token), token);
   }
   else 
   {
      workQueue.ContinueWork(t => DoWork(callback, token), token);
   }
}

oDoWork O método contém uma chamada de longa duração, portanto, não é tão simples como verificar constantemente o status detoken.IsCancellationRequested e fiança se / quando um cancelamento for detectado. O trabalho de longa duração bloqueará as continuações da tarefa até que termine, mesmo se a tarefa for cancelada.

Eu vim com dois métodos de amostra para solucionar esse problema, mas não estou convencido de que sejam adequados. Criei aplicativos de console simples para demonstrar como eles funcionam.

O ponto importante a ser observado é quea continuação é acionada antes que a tarefa original seja concluída.

Tentativa 1: uma tarefa interna

static void Main(string[] args)
{
   CancellationTokenSource cts = new CancellationTokenSource();
   var token = cts.Token;
   token.Register(() => Console.WriteLine("Token cancelled"));
   // Initial work
   var t = Task.Factory.StartNew(() =>
     {
        Console.WriteLine("Doing work");

      // Wrap the long running work in a task, and then wait for it to complete
      // or the token to be cancelled.
        var innerT = Task.Factory.StartNew(() => Thread.Sleep(3000), token);
        innerT.Wait(token);
        token.ThrowIfCancellationRequested();
        Console.WriteLine("Completed.");
     }
     , token);
   // Second chunk of work which, in the real world, would be identical to the
   // first chunk of work.
   t.ContinueWith((lastTask) =>
         {
             Console.WriteLine("Continuation started");
         });

   // Give the user 3s to cancel the first batch of work
   Console.ReadKey();
   if (t.Status == TaskStatus.Running)
   {
      Console.WriteLine("Cancel requested");
      cts.Cancel();
      Console.ReadKey();
   }
}

Isso funciona, mas a tarefa "interiorT" parece extremamente delicada para mim. Ele também tem a desvantagem de me forçar a refatorar todas as partes do meu código que funcionam na fila dessa maneira, exigindo o empacotamento de todas as chamadas de execução longa em uma nova tarefa.

Tentativa # 2: mexer em TaskCompletionSource

static void Main(string[] args)
{  var tcs = new TaskCompletionSource<object>();
//Wire up the token's cancellation to trigger the TaskCompletionSource's cancellation
   CancellationTokenSource cts = new CancellationTokenSource();
   var token = cts.Token;
   token.Register(() =>
         {   Console.WriteLine("Token cancelled");
             tcs.SetCanceled();
          });
   var innerT = Task.Factory.StartNew(() =>
      {
          Console.WriteLine("Doing work");
          Thread.Sleep(3000);
          Console.WriteLine("Completed.");
    // When the work has complete, set the TaskCompletionSource so that the
    // continuation will fire.
          tcs.SetResult(null);
       });
   // Second chunk of work which, in the real world, would be identical to the
   // first chunk of work.
   // Note that we continue when the TaskCompletionSource's task finishes,
   // not the above innerT task.
   tcs.Task.ContinueWith((lastTask) =>
      {
         Console.WriteLine("Continuation started");
      });
   // Give the user 3s to cancel the first batch of work
   Console.ReadKey();
   if (innerT.Status == TaskStatus.Running)
   {
      Console.WriteLine("Cancel requested");
      cts.Cancel();
      Console.ReadKey();
   }
}

Novamente isso funciona, mas agora tenho dois problemas:

a) Parece que estou abusando do TaskCompletionSource por nunca usar o resultado e definindo nulo quando terminar o meu trabalho.

b) Para conectar corretamente as continuações, preciso controlar o TaskCompletionSource exclusivo da unidade de trabalho anterior, e não a tarefa que foi criada para ele. Isso é tecnicamente possível, mas novamente parece estranho e desajeitado.

Para onde ir daqui?

Para reiterar, minha pergunta é: algum desses métodos é a maneira "correta" de resolver esse problema ou existe uma solução mais correta / elegante que me permita abortar prematuramente uma tarefa de longa duração e iniciar imediatamente uma continuação? Minha preferência é por uma solução de baixo impacto, mas eu estaria disposto a realizar uma grande refatoração, se for a coisa certa a fazer.

Como alternativa, o TPL é a ferramenta correta para o trabalho ou estou perdendo um mecanismo melhor de enfileiramento de tarefas. Minha estrutura de destino é o .NET 4.0.

questionAnswers(1)

yourAnswerToTheQuestion