OOP Course
Сьогодні

Підрозділ 16.1

Клас Task. Основи TPL

16.1. Клас Task. Основи TPL У попередньому розділі ми розглядали багатопоточність через клас Thread — прямий, але низькорівневий інструмент. Ми вручну створювали потоки, передавали делегати, викликали Join , пі

16.1. Клас Task. Основи TPL

У попередньому розділі ми розглядали багатопоточність через клас Thread — прямий, але низькорівневий інструмент. Ми вручну створювали потоки, передавали делегати, викликали Join(), піклувались про пріоритети і тип потоку (передній чи фоновий). Для простих сценаріїв це цілком прийнятно, але у реальних системах з десятками паралельних операцій такий підхід стає громіздким.

TPL (Task Parallel Library) — бібліотека паралельного програмування, що входить до складу .NET, починаючи з версії 4.0. Її мета — підняти рівень абстракції: замість того щоб думати про потоки, потокові пули і синхронізацію вручну, розробник описує завдання (tasks) — логічні одиниці роботи — і довіряє TPL ефективно їх виконати. TPL розміщена у просторі імен System.Threading.Tasks.

Thread vs Task: у чому різниця

Ключова відмінність між Thread і Task — у тому, як вони використовують ресурси операційної системи. Кожен Thread — це окремий потік ОС із власним стеком пам'яті (~1 МБ). Створення і знищення потоку — дорога операція. Якщо програма постійно створює і знищує сотні потоків, продуктивність падає.

Task натомість використовує пул потоків (ThreadPool) — набір вже створених потоків, які очікують нової роботи. Коли завдання завершується, потік не знищується, а повертається у пул і чекає наступного завдання. Це суттєво знижує накладні витрати на створення і знищення потоків. Крім того, Task підтримує повернення результатів, ланцюги продовжень і скасування — все те, що з Thread довелось би реалізовувати вручну.

Thread vs Task: модель виконання

Три способи створення завдання

Спосіб 1: конструктор + Start()

Конструктор + Start() — найбільш явний спосіб. Він дає контроль над моментом запуску: завдання можна створити заздалегідь і запустити пізніше. Task.CurrentId повертає числовий ідентифікатор поточного виконуваного завдання.

Спосіб 2: Task.Factory.StartNew()

Task.Factory.StartNew() — старіший API, що залишається актуальним для складних сценаріїв: він приймає додаткові параметри TaskCreationOptions і TaskScheduler, недоступні в Task.Run(). Для звичайних завдань зручніший Task.Run().

Спосіб 3: Task.Run() — сучасний рекомендований підхід

Task.Run() — найпростіший і найпоширеніший спосіб у сучасному коді. Він автоматично запускає завдання у пулі потоків без додаткових налаштувань. Це рекомендований підхід для переважної більшості сценаріїв.

Властивості класу Task

Кожен об'єкт Task має набір властивостей, що дозволяють отримати інформацію про стан і результат виконання:

Властивість Тип Опис
Id int Унікальний ідентифікатор завдання (статично для зовнішнього коду)
Status TaskStatus Поточний стан: Created, Running, RanToCompletion тощо
IsCompleted bool true якщо завдання завершилось (успішно, з помилкою або скасоване)
IsCompletedSuccessfully bool true тільки при успішному завершенні
IsCanceled bool true якщо завдання скасовано через CancellationToken
IsFaulted bool true якщо завдання завершилось із необробленим винятком
Exception AggregateException? Виняток (або null) — обгортка навколо всіх внутрішніх винятків

Статична властивість Task.CurrentId доступна лише всередині виконуваного завдання — повертає id поточного Task або null поза завданням.

Стани завдання (TaskStatus)

Завдання проходить через кілька станів протягом свого часу існування. Їх описує перерахування TaskStatus:

Стани завдання (TaskStatus)

  • Created — завдання створено через new Task(...), але ще не запущено. Стан перед Start().
  • WaitingForActivation — завдання очікує активації планувальником. У цей стан потрапляють завдання, створені через Task.Run() або ContinueWith().
  • WaitingToRun — завдання заплановано до виконання у пулі потоків, але вільний потік ще не отримано.
  • Running — завдання активно виконується у потоці пулу.
  • RanToCompletion — завдання успішно завершило виконання без винятків і скасування.
  • Canceled — завдання завершилось через скасування за допомогою CancellationToken.
  • Faulted — завдання завершилось із необробленим винятком.

Очікування завершення завдань

Wait() — очікування одного завдання

task.Wait() блокує поточний (головний) потік до завершення завдання. Якщо завдання завершилось з винятком, Wait() викидає AggregateException:

WaitAll() — очікування кількох завдань

Task.WaitAll(tasks) блокує поточний потік до завершення всіх переданих завдань:

WaitAny() — очікування першого завершеного

Task.WaitAny(tasks) повертає індекс першого завдання, що завершилось, і розблоковує поточний потік. Решта завдань продовжують виконуватись:

WaitAll підходить для сценарію «потрібні всі результати перед наступним кроком», WaitAny — для «почни обробку як тільки з'явиться перший результат» або реалізації тайм-аутів.

Синхронний запуск: RunSynchronously()

Метод RunSynchronously() виконує завдання синхронно в поточному потоці, без передачі до пулу потоків. Використовується рідко — наприклад, для тестування або коли потрібно явно контролювати, у якому потоці виконується завдання:

Обробка винятків у завданнях

Коли в завданні виникає виняток, він не зупиняє програму автоматично. Натомість він зберігається у властивості Task.Exception і повторно кидається при виклику Wait(), Result або доступі до Task.Exception. Виняток завжди обгорнутий у AggregateException:

AggregateException є оберткою, бо одне завдання теоретично може містити кілька вкладених завдань, кожне з яких може кинути свій виняток. InnerExceptions — колекція всіх внутрішніх винятків. У простих сценаріях вона містить один елемент.

Розроблено Tomka Yurii · © 2026 ·