Прерывание длительной задачи в TPL

Наше приложение использует 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.

17
задан Gennady Vanin Геннадий Ванин 7 April 2013 в 04:45
поделиться