run - kill task c#
Anulando una tarea de larga duraciĆ³n en TPL (1)
El problema real aquí es que la llamada de larga duración en DoWork no tiene en cuenta la cancelación. Si entiendo correctamente, lo que está haciendo aquí no es realmente cancelar el trabajo de larga duración, sino simplemente permitir que se ejecute la continuación y, cuando el trabajo finaliza en la tarea cancelada, ignorar el resultado. Por ejemplo, si usó el patrón de tarea interno para llamar a CrunchNumbers (), lo cual toma varios minutos, cancelar la tarea externa permitirá que ocurra la continuación, pero CrunchNumbers () continuará ejecutándose en segundo plano hasta que se complete.
No creo que haya otra forma real de evitar esto, aparte de que sus llamadas de larga duración admitan la cancelación. A menudo, esto no es posible (pueden estar bloqueando las llamadas a la API, sin compatibilidad con la API para la cancelación). Cuando este es el caso, es realmente una falla en la API; puede verificar si hay llamadas API alternativas que podrían usarse para realizar la operación de una manera que pueda cancelarse. Un enfoque de hackeo para esto es capturar una referencia al subproceso subyacente utilizado por la Tarea cuando se inicia la Tarea y luego llamar a Thread.Interrupt. Esto activará 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; no dude en editar la respuesta si la hace funcionar y tiene 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;
}
}
Nuestra aplicación utiliza el TPL para serializar (potencialmente) unidades de trabajo de larga ejecució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 ya no se requiere el trabajo actual, nos gustaría abandonar lo que estábamos haciendo e iniciar de inmediato una tarea diferente.
Las 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 duración, por lo que no es tan simple como verificar constantemente el estado del token.IsCancellationRequested
e token.IsCancellationRequested
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.
He encontrado dos métodos de muestra para solucionar este problema, pero no estoy convencido de que ninguno sea el adecuado. 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 se complete la tarea original .
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" me parece extremadamente complicada. También tiene el inconveniente de forzarme a refactorizar todas las partes de mi código que hacen cola de esta manera, al requerir el cierre de todas las llamadas de larga duración en una nueva tarea.
Intento n. ° 2: tareas de complicación
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();
}
}
Nuevamente esto funciona, pero ahora tengo dos problemas:
a) Se siente como si estuviera abusando de TaskCompletionSource al no usar nunca su resultado, y simplemente estableciendo nulo cuando termine mi trabajo.
b) Para cablear correctamente las continuaciones, necesito mantener un identificador de la única fuente de trabajo TaskCompletionSource anterior, y no la tarea que se creó para ello. Esto es técnicamente posible, pero una vez más se siente torpe y extraño.
A dónde ir desde aquí?
Para reiterar, mi pregunta es: ¿alguno de estos métodos es la manera "correcta" de abordar este problema, o existe una solución más correcta / elegante que me permita abortar prematuramente una tarea de larga duración e iniciar de inmediato una continuación? Mi preferencia es por una solución de bajo impacto, pero estaría dispuesta a emprender 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.