В нашем приложении используется 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();
}
}
Это работает, но задача "innerT" для меня очень полезна. У этого также есть недостаток, заставляющий меня реорганизовать все части моего кода, которые работают в очереди таким образом, путем необходимости обертывания всех длительных вызовов в новой Задаче.
Попытка # 2: TaskCompletionSource tinkering
static void Main(string[] args)
{ var tcs = new TaskCompletionSource<object>();
//Wire up the token cancellation to trigger the TaskCompletionSource 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 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.