Підрозділ 17.3
Послідовне та паралельне виконання. Task.WhenAll та Task.WhenAny
17.3. Послідовне та паралельне виконання. Task.WhenAll та Task.WhenAny У реальних асинхронних системах рідко виникає потреба виконати лише одну операцію. Зазвичай потрібно координувати кілька: завантажити кільк
17.3. Послідовне та паралельне виконання. Task.WhenAll та Task.WhenAny
У реальних асинхронних системах рідко виникає потреба виконати лише одну операцію. Зазвичай потрібно координувати кілька: завантажити кілька ресурсів, дочекатися першого результату з набору джерел, або збудувати складний конвеєр обробки. Для цього C# надає два потужні комбінатори: Task.WhenAll і Task.WhenAny.

Послідовне виконання — коли порядок має значення
Якщо кожна наступна операція залежить від результату попередньої, виконання мусить бути послідовним. await кожного кроку по черзі — природній спосіб висловити цю залежність:
Послідовне виконання — правильний вибір, коли операції пов'язані ланцюгом залежностей. Загальний час дорівнює сумі часів усіх кроків.
Паралельне виконання — коли операції незалежні
Якщо кілька операцій не залежать одна від одної, їх можна запустити одночасно. Ключова ідея: не await кожну операцію одразу — спочатку запусти всі, потім очікуй всіх:
Загальний час паралельного виконання — час найдовшої операції (~400 мс замість ~950 мс). Звісно, це справедливо тільки для справді незалежних операцій.
Task.WhenAll — очікування всіх завдань
Task.WhenAll — найчистіший спосіб запустити кілька завдань паралельно і дочекатися їх усіх. Він приймає колекцію Task або масив Task
Task.WhenAll з масивом однотипних завдань
Якщо всі завдання мають однаковий тип Task<T>, WhenAll повертає Task<T[]> — масив результатів у порядку вихідних завдань:
Обробка помилок у Task.WhenAll
Task.WhenAll дочікується всіх завдань, навіть якщо деякі завершились з помилкою. Після завершення він кидає AggregateException, що містить всі помилки від усіх завдань, що завершились невдало:
Важлива деталь: await Task.WhenAll(...) при помилці розгортає лише першу з виключень. Щоб отримати всі помилки, потрібно аналізувати task.Exception.InnerExceptions для кожного завдання після завершення WhenAll.
Task.WhenAny — очікування першого завдання
Task.WhenAny завершується, щойно перше з переданих завдань завершиться (успішно або з помилкою). Це корисно для сценаріїв: «отримати відповідь від найшвидшого сервера», «реалізувати тайм-аут», «обробляти результати по мірі готовності»:
Тайм-аут через Task.WhenAny + Task.Delay
Один з найпоширеніших патернів з WhenAny — реалізація тайм-ауту для асинхронної операції:
Зверніть: Task.WhenAny не скасовує інші завдання — SlowDatabaseQueryAsync продовжить виконуватись у фоні навіть після тайм-ауту. Щоб коректно скасувати, потрібно CancellationToken (розглядається у розділі 17.5).
Обробка результатів по мірі готовності
WhenAny дозволяє обробляти результати одразу, як вони з'являються, без очікування всіх:
Цей патерн дозволяє одразу реагувати на перші результати, не чекаючи найповільніших — важливо для інтерактивних систем.
Порівняння: WhenAll vs WhenAny
| Аспект | Task.WhenAll |
Task.WhenAny |
|---|---|---|
| Завершується коли | Всі задачі завершились | Перша задача завершилась |
| Результат | T[] (для Task<T>[]) |
Перша завершена Task<T> |
| Помилки | Збирає всі, кидає AggregateException | Тільки від першої завершеної |
| Типові сценарії | Паралельне збирання даних | Тайм-аут, перший відповів, обробка по черзі |
| Скасовує інші | Ні | Ні |
Обидва методи є фундаментальними інструментами координації async-операцій. Їх комбінація дозволяє будувати складні стратегії паралельного виконання без явного управління потоками.