Anulando una tarea de larga ejecución en TPL

Nuestra aplicación utiliza el TPL para serializar (potencialmente) unidades de trabajo de larga duración. La creación de trabajo (tareas) está dirigida por el usuario y puede cancelarse en cualquier momento. Para tener una interfaz de usuario receptiva, si el trabajo actual ya no es necesario, nos gustaría abandonar lo que estábamos haciendo e inmediatamente comenzar una tarea diferente.

Las tareas se ponen en cola de la siguiente manera:

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

losDoWork El método contiene una llamada de larga ejecución, por lo que no es tan simple como comprobar constantemente el estado detoken.IsCancellationRequested y rescatando si / cuando se detecta una cancelación. El trabajo de larga duración bloqueará las continuaciones de la tarea hasta que finalice, incluso si la tarea se cancela.

Se me ocurrieron dos métodos de muestra para solucionar este problema, pero no estoy convencido de que ninguno sea adecuado. Creé aplicaciones de consola simples para demostrar cómo funcionan.

El punto importante a tener en cuenta es quela continuación se dispara antes de que se complete la tarea original.

Intento n. ° 1: una tarea 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();
   }
}

Esto funciona, pero la tarea "innerT" me parece extremadamente difícil de entender. También tiene el inconveniente de obligarme a refactorizar todas las partes de mi código que ponen en cola el trabajo de esta manera, al exigir el cierre de todas las llamadas de larga ejecución en una nueva Tarea.

Intento # 2: retoques de 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();
   }
}

De nuevo, esto funciona, pero ahora tengo dos problemas:

a) Parece que estoy abusando de TaskCompletionSource al nunca usar su resultado, y simplemente establecer nulo cuando he terminado mi trabajo.

b) Para conectar correctamente las continuaciones, necesito controlar la TaskCompletionSource única de la unidad de trabajo anterior y no la tarea que se creó para ella. Esto es técnicamente posible, pero nuevamente se siente torpe y extraño.

A dónde ir desde aquí?

Para reiterar, mi pregunta es: ¿alguno de estos métodos es la forma "correcta" de abordar este problema, o hay una solución más correcta / elegante que me permita abortar prematuramente una tarea de larga duración e inmediatamente comenzar una continuación? Prefiero una solución de bajo impacto, pero estaría dispuesto a realizar una gran refactorización si es lo correcto.

Alternativamente, es el TPL incluso la herramienta correcta para el trabajo, o me falta un mejor mecanismo de cola de tareas. Mi marco de destino es .NET 4.0.

Respuestas a la pregunta(1)

Su respuesta a la pregunta