Наше приложение использует TPL для сериализации (потенциально) длительно выполняющихся единиц работы. Создание работы (задач) осуществляется пользователем и может быть отменено в любой момент. Чтобы иметь отзывчивый пользовательский интерфейс, если текущая часть работы больше не требуется, мы хотели бы отказаться от того, что мы делали, и немедленно запускать другую задачу.
Задачи выстраиваются в очередь примерно так:
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);
}
}
Метод DoWork
содержит длительный вызов, поэтому это не так просто, как постоянная проверка состояния ] token.IsCancellationRequested
и освобождение при обнаружении отмены. Длительная работа будет блокировать продолжения задач до их завершения, даже если задача будет отменена.
Я придумал два простых метода для решения этой проблемы, но не уверен, что они подходят. Я создал простые консольные приложения, чтобы продемонстрировать, как они работают.
Важно отметить, что продолжение срабатывает до завершения исходной задачи .
Попытка №1: внутренняя задача
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();
}
}
Это работает, но «внутренняя» задача кажется чрезвычайно запутанной. мне. У этого также есть недостаток, заставляющий меня реорганизовывать все части моего кода, которые ставятся в очередь, работать таким образом, требуя обертывания всех длительных вызовов в новой Задаче.
Попытка №2: Исправление 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();
}
}
Опять же, это работает, но теперь у меня две проблемы:
a) Такое ощущение, что я злоупотребляю TaskCompletionSource, никогда не использую его результат и просто устанавливаю null, когда закончу свою работу.
b) Чтобы правильно подключать продолжения. Мне нужно контролировать уникальный TaskCompletionSource предыдущей единицы работы, а не задачу, которая была создана для нее. Это технически возможно, но опять же кажется неуклюжим и странным.
Куда идти дальше?
Повторюсь, мой вопрос: является ли любой из этих методов «правильным» способом решения этой проблемы, или есть более правильное / элегантное решение, которое позволит мне преждевременно прервать длительную задачу и сразу начать ее продолжение? Я предпочитаю решение с низким уровнем воздействия, но я был бы готов провести серьезный рефакторинг, если это правильно.
С другой стороны, TPL - это даже правильный инструмент для работы, или мне не хватает улучшенный механизм очередности задач. Моя целевая платформа - .NET 4.0.