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