Підрозділ 16.3
Продовження завдань. ContinueWith
16.3. Продовження завдань. ContinueWith Завдання продовження continuation task — це завдання, яке автоматично запускається після завершення іншого завдання. Це ключовий механізм побудови послідовних ланцюгів об
16.3. Продовження завдань. ContinueWith
Завдання-продовження (continuation task) — це завдання, яке автоматично запускається після завершення іншого завдання. Це ключовий механізм побудови послідовних ланцюгів обробки (pipelines): результат одного кроку передається наступному, а кожен крок запускається лише тоді, коли попередній завершено. На відміну від Wait(), де поточний потік блокується в очікуванні, ContinueWith не блокує — він лише реєструє «що виконати потім» і повертає управління негайно.
Базовий синтаксис ContinueWith
Метод ContinueWith приймає делегат Action<Task> або Action<Task<T>> — параметр prevTask це посилання на попереднє завдання, що щойно завершилось. Через нього можна отримати Id, Status, Exception і, у разі Task<T>, Result попереднього кроку.
Передача результату між завданнями
Найпрактичніший сценарій ContinueWith — ланцюг обробки, де кожен крок отримує результат попереднього через Task<T>:

Довгий ланцюг — pipeline
Кілька ContinueWith можна об'єднати в лінійний ланцюг, де кожен наступний крок запускається після попереднього. Такий патерн називають pipeline (конвеєр):
Кожен крок отримує посилання на попередній Task (параметр t) і може перевірити його статус. Ланцюг виконується суворо послідовно, але не блокує головний потік між кроками — між кроками головний потік вільний.
TaskContinuationOptions: умовні продовження
За замовчуванням ContinueWith запускається після завершення попереднього завдання незалежно від результату — чи воно успішне, чи скасоване, чи завершилось з помилкою. TaskContinuationOptions дозволяє вказати умову, при якій продовження запуститься:
| Опція | Коли запускається |
|---|---|
OnlyOnRanToCompletion |
Тільки якщо попереднє завершилось успішно |
OnlyOnFaulted |
Тільки якщо попереднє завершилось з винятком |
OnlyOnCanceled |
Тільки якщо попереднє було скасовано |
NotOnRanToCompletion |
Якщо попереднє НЕ успішне |
NotOnFaulted |
Якщо попереднє НЕ з помилкою |
ExecuteSynchronously |
Виконати синхронно в тому ж потоці |
Зверніть: якщо умова TaskContinuationOptions не виконана, продовження переходить у стан Canceled (а не просто ігнорується). Тому WaitAll на набір умовних продовжень може кинути AggregateException з TaskCanceledException всередині — це очікувана поведінка, яку треба обробляти.
Обробка помилок у ланцюгу
Коли у ланцюгу виникає помилка і не перехоплюється всередині завдання, вона «підіймається» вгору через AggregateException. Правильний патерн — розмістити завдання-обробник помилок наприкінці ланцюга з OnlyOnFaulted:
Такий підхід дозволяє будувати надійні ланцюги з розгалуженням на успішний і помилковий шляхи, не вдаючись до вкладених try/catch всередині кожного кроку.
TaskCompletionSource — Task під ручним керуванням
Task.Run() і new Task(...) створюють завдання, що виконують певний делегат у ThreadPool. Але іноді потрібен Task, що завершується не через виконання делегата, а в момент, коли ми самі вирішуємо. Наприклад:
- Потрібно обернути callback-based API в async-стиль
- Потрібно «сигналізувати» між частинами системи через Task
- Потрібно створити Task, що чекає зовнішньої події (таймер, WebSocket-повідомлення, результат від іншого сервісу)
Для цього існує TaskCompletionSource<T> (або TaskCompletionSource без типового параметру для Task):
var tcs = new TaskCompletionSource<string>();
Task<string> task = tcs.Task; // Task, що "ще не завершений"
// Пізніше, в будь-якому місці:
tcs.SetResult("значення"); // Task завершується успішно
tcs.SetException(ex); // Task завершується з помилкою
tcs.SetCanceled(); // Task скасовуєтьсяTaskCompletionSource є «мостом» між callback/event-моделлю і сучасним async/await. Якщо ви отримали API зі старою моделлю (BeginXxx/EndXxx, EventHandler, callbacks) — TaskCompletionSource перетворює її у Task, який можна await-ити.
Важливо: після виклику SetResult, SetException або SetCanceled стан Task зафіксований — повторний виклик кине InvalidOperationException. Для «одноразового сигналу» є зручна альтернатива TrySetResult/TrySetException/TrySetCanceled, що повертають bool замість кидання винятку.