Подтвердить что ты не робот

Прерывание долговременной задачи в 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();
   }
}

Это работает, но задача "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.

4b9b3361

Ответ 1

Настоящая проблема заключается в том, что долговременный вызов 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;
    }
}