Отмена длительной задачи в TPL - PullRequest
17 голосов
/ 20 января 2011

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

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.

1 Ответ

9 голосов
/ 20 января 2011

Настоящая проблема здесь заключается в том, что длительный вызов в DoWork не учитывает отмену.Если я правильно понимаю, то, что вы делаете здесь, на самом деле не отменяет длительную работу, а просто позволяет продолжить выполнение и, когда работа завершается с отмененной задачей, игнорировать результат.Например, если вы использовали внутренний шаблон задачи для вызова CrunchNumbers (), что занимает несколько минут, отмена внешней задачи приведет к продолжению, но CrunchNumbers () продолжит выполняться в фоновом режиме до завершения.

Я не думаю, что есть какой-то реальный способ обойти это, кроме как сделать ваши длительные звонки поддержки отмены.Часто это невозможно (они могут блокировать вызовы API, без поддержки API для отмены.) В этом случае это действительно недостаток API;Вы можете проверить, есть ли альтернативные вызовы API, которые можно использовать для выполнения операции способом, который можно отменить.Один из подходов к взлому - захватить ссылку на базовый поток, используемый Задачей при запуске Задачи, и затем вызвать Thread.Interrupt.Это разбудит поток из различных состояний сна и позволит ему завершиться, но потенциально неприятным способом.В худшем случае вы даже можете вызвать Thread.Abort, но это еще более проблематично и не рекомендуется.


Вот укол в обертке на основе делегата.Это не проверено, но я думаю, что это сработает;не стесняйтесь редактировать ответ, если он работает и у вас есть исправления / улучшения.

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;
    }
}
...