2011-01-19 20 views
17

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

tareas se ponen en cola algo como esto:

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

El método DoWork contiene una llamada de larga ejecución, por lo que no es tan simple como comprobando constantemente el estado de token.IsCancellationRequested y el rescate si/cuando se detecta un cancel . El trabajo de larga duración bloqueará las continuación de tareas hasta que finalice, incluso si la tarea se cancela.

He encontrado dos métodos de muestra para solucionar este problema, pero no estoy seguro de que sean correctos. Creé aplicaciones de consola simples para demostrar cómo funcionan.

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

Intento # 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" se siente muy kludgey a mí. También tiene el inconveniente de obligarme a refactorizar todas las partes de mi código que hacen cola de esta manera, al necesitar el final de todas las llamadas de larga ejecución en una nueva Tarea.

Intento # 2: TaskCompletionSource retoques

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) Se siente como que estoy abusando TaskCompletionSource por no usar nunca es resultado, y acaba de establecer nulo cuando he terminado mi trabajo.

b) Para conectar correctamente las continuaciones, necesito mantener un control sobre el TaskCompletionSource único de la unidad de trabajo anterior, y no la tarea que se creó para él. Esto es técnicamente posible, pero nuevamente se siente torpe y extraño.

¿Dónde ir desde aquí?

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

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

+0

también hice la pregunta aquí: http://social.msdn.microsoft.com/Forums/en/parallelextensions/thread/d0bcb415-fb1e-42e4-90f8-c43a088537fb –

Respuesta

8

El verdadero problema aquí es que la llamada de larga ejecución en DoWork no es compatible con la cancelación. Si lo entiendo correctamente, lo que está haciendo aquí no es cancelar el trabajo prolongado, sino simplemente permitir que se ejecute la continuación y, cuando el trabajo finalice en la tarea cancelada, ignorar el resultado.Por ejemplo, si utilizó el patrón de tareas interno para llamar a CrunchNumbers(), que demora varios minutos, cancelar la tarea externa permitirá que continúe, pero CrunchNumbers() continuará ejecutándose en segundo plano hasta su finalización.

No creo que haya otra forma de evitar esto que la cancelación de soporte de llamadas de larga duración. A menudo, esto no es posible (pueden estar bloqueando las llamadas a la API, sin soporte API para la cancelación). Cuando este es el caso, es realmente un defecto en la API; Puede verificar si hay llamadas API alternativas que podrían usarse para realizar la operación de una manera que se puede cancelar. Un enfoque de hackeo para esto es capturar una referencia al subproceso subyacente que está siendo utilizado por la tarea cuando se inicia la tarea y luego llamar a Thread.Interrupt. Esto despertará el hilo de varios estados de suspensión y permitirá que termine, pero de una manera potencialmente desagradable. En el peor de los casos, incluso puedes llamar a Thread.Abort, pero eso es aún más problemático y no recomendado.


Aquí hay una puñalada en un contenedor basado en delegado. No está probado, pero creo que hará el truco; siéntete libre de editar la respuesta si la haces funcionar y tienes correcciones/mejoras.

public sealed class AbandonableTask 
{ 
    private readonly CancellationToken _token; 
    private readonly Action _beginWork; 
    private readonly Action _blockingWork; 
    private readonly Action<Task> _afterComplete; 

    private AbandonableTask(CancellationToken token, 
          Action beginWork, 
          Action blockingWork, 
          Action<Task> afterComplete) 
    { 
     if (blockingWork == null) throw new ArgumentNullException("blockingWork"); 

     _token = token; 
     _beginWork = beginWork; 
     _blockingWork = blockingWork; 
     _afterComplete = afterComplete; 
    } 

    private void RunTask() 
    { 
     if (_beginWork != null) 
      _beginWork(); 

     var innerTask = new Task(_blockingWork, 
           _token, 
           TaskCreationOptions.LongRunning); 
     innerTask.Start(); 

     innerTask.Wait(_token); 
     if (innerTask.IsCompleted && _afterComplete != null) 
     { 
      _afterComplete(innerTask); 
     } 
    } 

    public static Task Start(CancellationToken token, 
          Action blockingWork, 
          Action beginWork = null, 
          Action<Task> afterComplete = null) 
    { 
     if (blockingWork == null) throw new ArgumentNullException("blockingWork"); 

     var worker = new AbandonableTask(token, beginWork, blockingWork, afterComplete); 
     var outerTask = new Task(worker.RunTask, token); 
     outerTask.Start(); 
     return outerTask; 
    } 
} 
+0

Su interpretación es correcta. Estamos de acuerdo con permitir que nuestros "CrunchNumbers" se completen, pero ese resultado será ignorado cada vez que entre. –

+0

Si ese es el caso, creo que su patrón interno de tareas es el más limpio. En mi opinión, mapea lo más lógicamente, con lo que realmente estás haciendo. Estás lanzando una tarea de larga duración como parte de tu operación y, si se cancela, esencialmente "abandonas" esa tarea interna y continúas. Creo que puede mejorar algo de la incomodidad del patrón al encapsularlo en una clase de ayuda genérica; me vienen a la mente algunos diseños posibles, pero estoy seguro de que puedes encontrar algo que se adapte perfectamente al código real que necesitarás cambiar. –

+0

Gracias por el código de envoltura - las distracciones masivas me impidieron probarlo ayer - con suerte el día de hoy estará más sano. Actualmente estoy buscando completar mi intento n. ° 2 (el suyo es un resumen del intento n. ° 1), así que una vez que haya terminado, también experimentaré con su código. También es de destacar que recibí una respuesta de Stephen Toub aquí: http://social.msdn.microsoft.com/Forums/en/parallelextensions/thread/d0bcb415-fb1e-42e4-90f8-c43a088537fb –

Cuestiones relacionadas