Токен аннулирования в конструкторе задач: почему?

223

Некоторые System.Threading.Tasks.Taskконструкторы принимают CancellationTokenв качестве параметра:

CancellationTokenSource source = new CancellationTokenSource();
Task t = new Task (/* method */, source.Token);

Что меня сбивает с толку, так это то, что изнутри тела метода не существует способа фактически получить переданный токен (например, ничего подобного Task.CurrentTask.CancellationToken). Токен должен быть предоставлен через какой-то другой механизм, такой как объект состояния, или захвачен в лямбду.

Итак, для чего служит предоставление токена отмены в конструкторе?

Colin
источник

Ответы:

254

Передача CancellationTokenв Taskконструктор связывает его с задачей.

Цитирую ответ Стивена Тауба из MSDN :

Это имеет два основных преимущества:

  1. Если токен требует отмены до Taskначала выполнения, Taskон не будет выполнен. Вместо того, чтобы перейти к Running, он сразу перейдет к Canceled. Это позволяет избежать затрат на выполнение задачи, если она все равно будет отменена во время выполнения.
  2. Если тело задачи также отслеживает токен отмены и выбрасывает OperationCanceledExceptionсодержащий этот токен (что и ThrowIfCancellationRequestedделает), то, когда задача видит это OperationCanceledException, она проверяет, соответствует ли OperationCanceledExceptionтокен токена задачи. Если это так, то это исключение рассматривается как подтверждение совместного отмены и Taskперехода в Canceled состояние (а не в Faultedсостояние).
Макс Галкин
источник
2
TPL так хорошо продуман.
полковник Паник
1
Я предполагаю , что выгода 1 применяется аналогично пропусканием токен отмены , чтобы Parallel.ForилиParallel.ForEach
полковник Panic
27

Конструктор использует токен для внутренней обработки отмены. Если ваш код хочет получить доступ к токену, вы несете ответственность за его передачу. Я очень рекомендую прочитать книгу « Параллельное программирование с Microsoft .NET» в CodePlex .

Пример использования CTS из книги:

CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken token = cts.Token;

Task myTask = Task.Factory.StartNew(() =>
{
    for (...)
    {
        token.ThrowIfCancellationRequested();

        // Body of for loop.
    }
}, token);

// ... elsewhere ...
cts.Cancel();
user7116
источник
3
и что будет, если вы не передадите токен в качестве параметра? Похоже, поведение будет таким же, без цели.
sergtk
2
@sergdev: вы передаете токен, чтобы зарегистрировать его в задании и планировщике. Не передавать его и использовать его было бы неопределенным поведением.
user7116
3
@sergdev: после тестирования: myTask.IsCanceled и myTask.Status не совпадают, если вы не передаете токен в качестве параметра. Статус будет сбой, а не отменен. Тем не менее, исключение одно и то же: это исключение OperationCanceledException в обоих случаях.
Оливье де Ривойр
2
Что если я не позвоню token.ThrowIfCancellationRequested();? В моем тесте поведение такое же. Любые идеи?
Machinarium
1
@CobaltBlue: Нет when cts.Cancel() is called the Task is going to get canceled and end, no matter what you do. Если задание отменено до его запуска, оно отменяется . Если тело Задачи просто никогда не проверяет токен, оно будет выполнено до завершения, что приведет к состоянию RanToCompletion . Если тело выбрасывает OperationCancelledException, например, by ThrowIfCancellationRequested, то Task проверяет, совпадает ли CancellationToken этого исключения с тем, который связан с Task. Если это так, задача отменяется . Если нет, то это вина .
Wolfzoon
7

Отмена - не простой случай, как многие могут подумать. Некоторые из тонкостей объяснены в этом сообщении в блоге на MSDN:

Например:

В некоторых ситуациях в Parallel Extensions и в других системах необходимо активировать заблокированный метод по причинам, которые не вызваны явным отменой пользователем. Например, если один поток заблокирован blockingCollection.Take()из-за того, что коллекция пуста, а другой поток впоследствии вызывает blockingCollection.CompleteAdding(), тогда первый вызов должен проснуться и выдать a, InvalidOperationExceptionчтобы представить неправильное использование.

Отмена в параллельных расширениях

x0n
источник
4

Вот пример , который демонстрирует две точки в ответ по Макс Галкин :

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("*********************************************************************");
        Console.WriteLine("* Start canceled task, don't pass token to constructor");
        Console.WriteLine("*********************************************************************");
        StartCanceledTaskTest(false);
        Console.WriteLine();

        Console.WriteLine("*********************************************************************");
        Console.WriteLine("* Start canceled task, pass token to constructor");
        Console.WriteLine("*********************************************************************");
        StartCanceledTaskTest(true);
        Console.WriteLine();

        Console.WriteLine("*********************************************************************");
        Console.WriteLine("* Throw if cancellation requested, don't pass token to constructor");
        Console.WriteLine("*********************************************************************");
        ThrowIfCancellationRequestedTest(false);
        Console.WriteLine();

        Console.WriteLine("*********************************************************************");
        Console.WriteLine("* Throw if cancellation requested, pass token to constructor");
        Console.WriteLine("*********************************************************************");
        ThrowIfCancellationRequestedTest(true);
        Console.WriteLine();

        Console.WriteLine();
        Console.WriteLine("Test Done!!!");
        Console.ReadKey();
    }

    static void StartCanceledTaskTest(bool passTokenToConstructor)
    {
        Console.WriteLine("Creating task");
        CancellationTokenSource tokenSource = new CancellationTokenSource();
        Task task = null;
        if (passTokenToConstructor)
        {
            task = new Task(() => TaskWork(tokenSource.Token, false), tokenSource.Token);

        }
        else
        {
            task = new Task(() => TaskWork(tokenSource.Token, false));
        }

        Console.WriteLine("Canceling task");
        tokenSource.Cancel();

        try
        {
            Console.WriteLine("Starting task");
            task.Start();
            task.Wait();
        }
        catch (Exception ex)
        {
            Console.WriteLine("Exception: {0}", ex.Message);
            if (ex.InnerException != null)
            {
                Console.WriteLine("InnerException: {0}", ex.InnerException.Message);
            }
        }

        Console.WriteLine("Task.Status: {0}", task.Status);
    }

    static void ThrowIfCancellationRequestedTest(bool passTokenToConstructor)
    {
        Console.WriteLine("Creating task");
        CancellationTokenSource tokenSource = new CancellationTokenSource();
        Task task = null;
        if (passTokenToConstructor)
        {
            task = new Task(() => TaskWork(tokenSource.Token, true), tokenSource.Token);

        }
        else
        {
            task = new Task(() => TaskWork(tokenSource.Token, true));
        }

        try
        {
            Console.WriteLine("Starting task");
            task.Start();
            Thread.Sleep(100);

            Console.WriteLine("Canceling task");
            tokenSource.Cancel();
            task.Wait();
        }
        catch (Exception ex)
        {
            Console.WriteLine("Exception: {0}", ex.Message);
            if (ex.InnerException != null)
            {
                Console.WriteLine("InnerException: {0}", ex.InnerException.Message);
            }
        }

        Console.WriteLine("Task.Status: {0}", task.Status);
    }

    static void TaskWork(CancellationToken token, bool throwException)
    {
        int loopCount = 0;

        while (true)
        {
            loopCount++;
            Console.WriteLine("Task: loop count {0}", loopCount);

            token.WaitHandle.WaitOne(50);
            if (token.IsCancellationRequested)
            {
                Console.WriteLine("Task: cancellation requested");
                if (throwException)
                {
                    token.ThrowIfCancellationRequested();
                }

                break;
            }
        }
    }
}

Вывод:

*********************************************************************
* Start canceled task, don't pass token to constructor
*********************************************************************
Creating task
Canceling task
Starting task
Task: loop count 1
Task: cancellation requested
Task.Status: RanToCompletion

*********************************************************************
* Start canceled task, pass token to constructor
*********************************************************************
Creating task
Canceling task
Starting task
Exception: Start may not be called on a task that has completed.
Task.Status: Canceled

*********************************************************************
* Throw if cancellation requested, don't pass token to constructor
*********************************************************************
Creating task
Starting task
Task: loop count 1
Task: loop count 2
Canceling task
Task: cancellation requested
Exception: One or more errors occurred.
InnerException: The operation was canceled.
Task.Status: Faulted

*********************************************************************
* Throw if cancellation requested, pass token to constructor
*********************************************************************
Creating task
Starting task
Task: loop count 1
Task: loop count 2
Canceling task
Task: cancellation requested
Exception: One or more errors occurred.
InnerException: A task was canceled.
Task.Status: Canceled


Test Done!!!
Элиаху Аарон
источник