Как безопасно отменить задачу с помощью CancellationToken и ждать Task.WhenAll

У меня есть структура, которая создает CancellationTokenSource, настраивает CancelAfter, затем вызывает асинхронный метод и передает токен. Затем метод async порождает множество задач, передавая токен отмены каждой из них, а затем ожидает сбора задач. Каждая из этих задач содержит логику для корректной отмены путем опроса IsCancellationRequested.

Моя проблема в том, что если я передаю CancellationToken в Task.Run (), возникает исключение AggregateException, содержащее TaskCanceledException. Это предотвращает корректную отмену задач.

Чтобы обойти это, я не могу передать CancelationToken в Task.Run, однако я не уверен, что буду терять. Например, мне нравится идея, что если моя задача зависает и не может выполнить плавную отмену, это исключение приведет к ее отключению. Я думал, что могу связать два токена CancelationToken, чтобы справиться с этим, один «изящный», а другой «принудительный». Однако мне это решение не нравится.

Вот какой-то псудокод, представляющий то, что я описал выше ..

public async Task Main()
{
    CancellationTokenSource cts = new CancellationTokenSource();
    cts.CancelAfter(30000);
    await  this.Run(cts.Token);
}

public async Task Run(CancellationToken cancelationToken)
{
    HashSet<Task> tasks = new HashSet<Task>();
    foreach (var work in this.GetWorkNotPictured)
    {
        // Here is where I could pass the Token, 
        //   however If I do I cannot cancel gracefully
        //   My dilemma here is by not passing I lose the ability to force
        //   down the thread (via exception) if         
        //   it's hung for whatever reason
        tasks.Add(Task.Run(() => this.DoWork(work, cancelationToken))
    }

    await Task.WhenAll(tasks);

    // Clean up regardless of if we canceled
    this.CleanUpAfterWork();

    // It is now safe to throw as we have gracefully canceled
    cancelationToken.ThrowIfCancellationRequested();
}

public static void DoWork(work, cancelationToken)
{
    while (work.IsWorking)
    {
        if (cancelationToken.IsCancellationRequested)
          return // cancel gracefully
        work.DoNextWork();
    }
}

person thaynes    schedule 15.06.2015    source источник


Ответы (2)


Я рекомендую вам следовать стандартному шаблону отмены исключения, а не просто возвращать:

public static void DoWork(work, cancellationToken)
{
  while (work.IsWorking)
  {
    cancellationToken.ThrowIfCancellationRequested();
    work.DoNextWork();
  }
}

Если у вас есть работа по очистке, это то, для чего finally (или using, если вы можете провести рефакторинг таким образом):

public async Task Run(CancellationToken cancellationToken)
{
  HashSet<Task> tasks = new HashSet<Task>();
  foreach (var work in this.GetWorkNotPictured)
  {
    tasks.Add(Task.Run(() => this.DoWork(work, cancellationToken))
  }

  try
  {
    await Task.WhenAll(tasks);
  }
  finally
  {
    this.CleanUpAfterWork();
  }
}
person Stephen Cleary    schedule 15.06.2015
comment
Спасибо за ответ! Я согласен с этим подходом, очистку - это то, что я всегда хотел бы делать, поэтому было бы уместно иметь файл finally. 2 вопроса к вам: 1. Нужно ли вообще проверять cancellationToken.ThrowIfCancellationRequested ()? Разве это не будет брошено Task.Run после того, как токен был передан? 2. Под рефакторингом для использования using вы имеете в виду выполнение очистки, когда класс, выполняющий эту работу, удаляется? - person thaynes; 15.06.2015
comment
1) Нет, токен отмены, переданный Task.Run, наблюдается только до начала выполнения делегата; Я описываю это подробнее в CancellationToken часть моего сообщения в блоге о делегировании задач. 2) Да. - person Stephen Cleary; 15.06.2015
comment
@StephenCleary, как отменить дочерний поток (конкретную задачу) и продолжить основной поток с дочерними задачами? Я задал вопрос здесь stackoverflow.com/questions/49217722/ - person immayankmodi; 11.03.2018

Предоставьте CancellationToken Task.Run в дополнение к передаче его методу, выполняющему работу. Когда вы сделаете это Task.Run, вы увидите, что выброшенное исключение было вызвано CancellationToken, которое оно было задано, и отметит Task как отмененное.

tasks.Add(Task.Run(() => this.DoWork(work, cancelationToken),
    cancelationToken));

Как только вы это сделаете, вы можете убедиться, что DoWork срабатывает при отмене токена, вместо того, чтобы проверять IsCancellationRequested, чтобы попытаться завершить, пометив как «завершено успешно».

person Servy    schedule 15.06.2015
comment
Проблема, с которой я столкнулся с передачей токена в Task.Run, заключалась в том, что Run генерировал TaskCanceledException и предотвращал логику CleanUpAfterWork. - person thaynes; 15.06.2015
comment
@thaynes Затем вы либо перехватываете исключение, если хотите продолжить нормальную работу с этого момента, либо используете finally, чтобы убедиться, что очистка выполняется, даже если работа выдает ошибку. - person Servy; 15.06.2015
comment
@thaynes На самом деле в этом нет ничего специфичного для асинхронного программирования. Вы всегда должны использовать finally / using, чтобы обеспечить выполнение очистки, даже если есть исключение. - person Servy; 15.06.2015