Маркер відміни в конструкторі завдань: чому?


223

Деякі System.Threading.Tasks.Taskконструктори приймають CancellationTokenза параметр:

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

Що мене бентежить з цього приводу, це те, що зсередини тіла методу не існує можливості реально дістатися до переданого маркера (наприклад, нічого подібного Task.CurrentTask.CancellationToken). Маркер повинен надаватися через якийсь інший механізм, наприклад, об'єкт стану або захоплений у лямбда.

Тож якою метою служить надання маркера скасування в конструкторі?

Відповіді:


254

Передача CancellationTokenв Taskконструктор пов'язує його із завданням.

Цитуючи відповідь Стівена Туба з MSDN :

Це має дві основні переваги:

  1. Якщо маркер має скасування запиту до Taskпочатку виконання, воно Taskне виконується. Замість того, щоб перейти до Running, воно негайно перейде до Canceled. Це дозволяє уникнути витрат на виконання завдання, якщо воно просто скасується під час виконання роботи.
  2. Якщо тіло завдання також контролює маркер відміни і кидає OperationCanceledExceptionмістить маркер, що містить цей вміст (що і ThrowIfCancellationRequestedробиться), тоді, коли завдання бачить це OperationCanceledException, він перевіряє, чи OperationCanceledExceptionмаркер 's відповідає маркеру завдання. У такому випадку виняток розглядається як підтвердження відмови від співпраці та Taskпереходу до Canceled держави (а не до Faultedдержави).

2
TPL настільки добре продуманий.
Полковник Паніка

1
Я припускаю, що вигода 1 застосовується аналогічно до передачі токена скасування до Parallel.ForабоParallel.ForEach
полковника Паніки

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();

3
і що станеться, якщо ви не передаєте маркер як параметр? Схоже, поведінка буде однаковою, без мети.
sergtk

2
@sergdev: ви передаєте маркер, щоб зареєструвати його із завданням та планувальником. Не передати його та використовувати його було б невизначеною поведінкою.
user7116

3
@sergdev: після тестування: myTask.IsCanceled і myTask.Status не однакові, коли ви не передаєте маркер як параметр. Статус буде замінено, а не скасовано. Тим не менш виняток той самий: це операціяCanceledException в обох випадках.
Олів'є де Рівойре

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, наприклад, за допомогою ThrowIfCancellationRequested, Задача перевірить, чи скасовано Виключення винятку таке, як те, що пов'язане із Завданням. Якщо це так, завдання скасовується . Якщо ні, це несправедливо .
Вольфзун

7

Скасування - це не простий випадок, як багато хто може подумати. Деякі тонкощі пояснені в цій публікації в блозі на msdn:

Наприклад:

У певних ситуаціях у паралельних розширеннях та інших системах необхідно прокинути заблокований метод з причин, які не пов’язані з явним скасуванням користувачем. Наприклад, якщо один потік заблокований blockingCollection.Take()через те, що колекція порожня, а інший потік згодом викликає blockingCollection.CompleteAdding(), то перший виклик повинен прокинутися і кинути знак, InvalidOperationExceptionщоб представляти неправильне використання.

Скасування в паралельних розширеннях


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!!!
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.