Підрозділ 16.5
Скасування завдань. CancellationToken
16.5. Скасування завдань. CancellationToken У реальних системах завдання нерідко потрібно зупинити достроково: користувач натиснув «Скасувати», сплив тайм аут, виникла аварійна ситуація або система перевантажен
16.5. Скасування завдань. CancellationToken
У реальних системах завдання нерідко потрібно зупинити достроково: користувач натиснув «Скасувати», сплив тайм-аут, виникла аварійна ситуація або система перевантажена. Примусове завершення потоку через Thread.Abort() давно застаріло, а в .NET Core і .NET 5+ не підтримується (метод лишився, але кидає PlatformNotSupportedException): воно небезпечне, бо залишає ресурси неприбраними. Правильний підхід — кооперативне скасування через CancellationToken.
Концепція кооперативного скасування полягає в тому, що завдання саме перевіряє, чи не надійшов сигнал скасування, і коректно завершується у зручний момент — після звільнення ресурсів, запису незавершеного результату, закриття з'єднань. Жодного примусового переривання — лише добровільна відповідь на запит.
Ключові типи
| Тип | Роль |
|---|---|
CancellationTokenSource |
Джерело сигналу скасування. Тільки він може викликати Cancel() |
CancellationToken |
Токен — «квиток» з інформацією про скасування. Передається у завдання |
OperationCanceledException |
Виняток, що сигналізує про кооперативне скасування |
Розподіл ролей чіткий: той хто управляє (зовнішній код) тримає CancellationTokenSource і вирішує, коли скасувати. Завдання отримує лише CancellationToken — воно може лише перевірити стан, але не може само себе скасувати через чужий джерело.


Спосіб 1: м'яке скасування через IsCancellationRequested
Найпростіший підхід — завдання регулярно перевіряє token.IsCancellationRequested і виходить через return при виявленні сигналу. Стан завдання після цього — RanToCompletion (успішне завершення), оскільки виняток не кидався:
М'яке скасування підходить для довгих циклічних операцій, де є зручна точка перевірки між ітераціями.
Спосіб 2: жорстке скасування через ThrowIfCancellationRequested
token.ThrowIfCancellationRequested() кидає OperationCanceledException, якщо надійшов сигнал скасування. Завдання переходить у стан Canceled — але лише за умови, що той самий токен передано і в Task.Run(..., token) другим аргументом (як у прикладі нижче). Якщо токен лише захоплено лямбдою, TPL не може зв'язати виняток зі скасуванням саме цієї задачі, і вона опиниться в стані Faulted. При виклику Wait() або Result зовнішній код отримає AggregateException з TaskCanceledException всередині:
Жорстке скасування підходить для задач, де після отримання сигналу скасування продовжувати виконання немає сенсу і завдання може бути перервано в будь-якій точці перевірки.
Перевірка стану: IsCanceled vs IsFaulted
Реєстрація обробника: Register()
CancellationToken.Register() дозволяє зареєструвати делегат, який буде викликаний при скасуванні. Це аналог події скасування — корисний для звільнення ресурсів, логування або надсилання повідомлень:
Обробник Register() викликається синхронно в тому потоці, що викликав Cancel(). Якщо зареєстровано кілька обробників, вони викликаються у зворотному порядку реєстрації (останній зареєстрований — першим), а покладатися на конкретний порядок взагалі не рекомендовано. Якщо токен уже скасовано на момент Register(), обробник виконується одразу, у потоці, що реєструє.
CancelAfter — автоматичне скасування за тайм-аутом
CancellationTokenSource.CancelAfter(ms) автоматично надсилає сигнал скасування через вказану кількість мілісекунд. Це стандартний спосіб реалізації тайм-аутів:
Скасування у Parallel.For і Parallel.ForEach
CancellationToken передається у Parallel через ParallelOptions. При скасуванні Parallel зупиняє нові ітерації і кидає OperationCanceledException:
Пов'язані токени (Linked tokens)
Іноді потрібно об'єднати кілька джерел скасування: наприклад, зовнішній тайм-аут або натискання «Скасувати». CancellationTokenSource.CreateLinkedTokenSource створює новий токен, що спрацьовує при скасуванні будь-якого з переданих:
Пов'язані токени — потужний інструмент для реалізації складних стратегій скасування, де кілька незалежних подій можуть ініціювати зупинку однієї операції. Після завершення їх потрібно утилізувати через Dispose() (або using), щоб звільнити ресурси.