1. Task.WhenAll: дождаться всех сразу
В реальной жизни редко бывает так, что вы делаете только что-то одно. Вы встаёте с кровати и при этом смотрите на часы, принимаете душ и поёте, одеваетесь и слушаете музыку, параллельно готовя себе кофе. Или вот если работаете на компьютере, то одновременно загружаете несколько файлов, отправляете пачку сетевых запросов, рассчитываете кучу данных по разным направлениям. Все эти действия могут (и должны!) выполняться параллельно, чтобы не тратить время пользователя зря. Мы не хотим ждать, пока каждая задача выполнится одна за другой! Как организовать такой "оркестр"? Как понять, когда ВСЁ завершилось? Или, напротив, узнать, кто первый справился?
Task.WhenAll и Task.WhenAny — инструменты, которые и нужны для этих сценариев.
Метод WhenAll
Task.WhenAll — это статический метод класса Task, который принимает коллекцию задач и возвращает новую задачу, завершающуюся только тогда, когда завершились все переданные задачи. Это как если бы на экзамене вы ждали, пока все однокурсники сдадут свои билеты, прежде чем выйти из аудитории.
Сигнатура метода
Task Task.WhenAll(params Task[] tasks)
Task<TResult[]> Task.WhenAll<TResult>(params Task<TResult>[] tasks)
Версии бывают для задач, возвращающих результаты (Task<TResult>) и "пустых" (Task).
Простейший пример: ждать загрузки всех файлов
Давайте попробуем загрузить три файла асинхронно. Для простоты — имитируем загрузку задержкой (Task.Delay). Примеры реальны, код рабочий.
using System;
using System.Threading.Tasks;
class Program
{
// Симулируем загрузку файла с задержкой, возвращаем имя
static async Task<string> DownloadFileAsync(string fileName)
{
Console.WriteLine($"► Начинаем загрузку: {fileName}");
await Task.Delay(2000); // Ждем 2 секунды
Console.WriteLine($"✓ Загрузка завершена: {fileName}");
return fileName;
}
static async Task Main()
{
var files = new[] { "fileA.txt", "fileB.txt", "fileC.txt" };
// Стартуем все загрузки одновременно
var downloadTasks = new Task<string>[files.Length];
for (int i = 0; i < files.Length; i++)
{
downloadTasks[i] = DownloadFileAsync(files[i]);
}
// Ожидаем завершения всех загрузок
string[] results = await Task.WhenAll(downloadTasks);
Console.WriteLine($"Все загрузки завершены! Список: {string.Join(", ", results)}");
}
}
Что здесь происходит?
- Мы не ждём каждую задачу по очереди. Все три запускаются параллельно (асинхронно).
- Task.WhenAll(downloadTasks) возвращает задачу, которая завершится только тогда, когда все задачи завершатся.
- После этого можем работать с результатами всех задач — использовать их, выводить на экран, отправлять дальше.
Аналогия
Это похоже на то, как если вы поручили трём курьерам доставить три посылки и хотите известить начальника только тогда, когда все успешно доберутся до клиентов.
Схема работы Task.WhenAll
sequenceDiagram
participant Program
participant Задача1
participant Задача2
participant Задача3
Program->>Задача1: Запуск
Program->>Задача2: Запуск
Program->>Задача3: Запуск
Задача1-->>Program: Завершена (может быть раньше остальных)
Задача2-->>Program: Завершена
Задача3-->>Program: Завершена
Program->>Program: Task.WhenAll завершён, все результаты доступны
Что, если одна из задач завершается с ошибкой?
Task.WhenAll не прерывается, когда какая-то задача завершилась с ошибкой. Он ждёт окончания всех задач. Если хотя бы в одной случилось исключение — итоговая задача будет в состоянии Faulted, и в ней будет коллекция всех исключений, которые произошли.
Пример
static async Task<string> MayThrowAsync(string fileName)
{
await Task.Delay(500);
if (fileName == "fileB.txt")
throw new Exception("Ошибка загрузки fileB.txt");
return fileName;
}
static async Task Main()
{
var files = new[] { "fileA.txt", "fileB.txt", "fileC.txt" };
var downloadTasks = files.Select(MayThrowAsync).ToArray();
try
{
string[] results = await Task.WhenAll(downloadTasks);
Console.WriteLine($"Все хорошо: {string.Join(", ", results)}");
}
catch (Exception ex)
{
Console.WriteLine($"Хотя бы одна задача завершилась с ошибкой: {ex.Message}");
}
}
В случае исключения, чтобы посмотреть все внутренние ошибки, используйте AggregateException.InnerExceptions.
Официальная документация: Task.WhenAll docs
2. Task.WhenAny: дождаться первой завершившейся задачи
Task.WhenAny — тоже статический метод, но он завершит свою работу, как только любая из переданных задач перейдёт в состояние "Завершена" (неважно, с ошибкой или успешно). Возвращает ссылку на первую завершившуюся задачу.
Сигнатура
Task<Task> Task.WhenAny(params Task[] tasks)
Task<Task<TResult>> Task.WhenAny<TResult>(params Task<TResult>[] tasks)
Аналогия
Кто первый приготовит пирог — тот и молодец, а остальных можно попросить остановиться. Иногда достаточно узнать, кто из нескольких серверов быстрее дал ответ, и использовать именно его результат.
Пример: кто быстрее?
using System;
using System.Threading.Tasks;
class Program
{
// Симулируем запрос с разной скоростью
static async Task<string> RequestAsync(string name, int delay)
{
await Task.Delay(delay);
return $"{name} завершился за {delay} мс";
}
static async Task Main()
{
var taskA = RequestAsync("A", 1000);
var taskB = RequestAsync("B", 700); // Быстрее всех
var taskC = RequestAsync("C", 1500);
// Ждем первую завершившуюся задачу
Task<string> finished = await Task.WhenAny(taskA, taskB, taskC);
Console.WriteLine($"Первым завершился: {finished.Result}");
}
}
С помощью WhenAny мы узнаём, кто из задач пришёл к финишу первым. После этого можно отменить остальные задачи, например, с помощью CancellationToken (тема следующих лекций).
Почему WhenAny возвращает именно задачу, а не результат?
Потому что метод не знает, какой тип результата у задачи: он не может заранее знать, какая из задач завершится первой. Поэтому он возвращает саму задачу — дальше нам нужно самостоятельно извлечь результат: через await или свойство Result у этой задачи.
WhenAll vs WhenAny: сравнительная иллюстрация
| Метод | Когда срабатывает | Что возвращает | Типичный сценарий |
|---|---|---|---|
|
Когда все завершились | Задача (Task/Task<T[]>) | Ждём всех ответов или всех файлов, агрегируем результаты |
|
Когда какая-то из задач завершилась | Задача самой первой завершившейся задачи | Используем первый результат, отменяем остальные |
3. Комбинируем WhenAll и WhenAny — жизненные сценарии
Иногда нужно сначала дождаться любой задачи, а потом всех. Или наоборот.
Пример: "Пинг-понг" между двумя серверами
Представьте, что вы отправляете запросы сразу двум серверам-клонам на случай, если один тормозит. Но результат используете первый.
static async Task Main()
{
var fastServer = RequestAsync("Быстрый сервер", 400); // 400 мс
var slowServer = RequestAsync("Медленный сервер", 2000); // 2000 мс
var completed = await Task.WhenAny(fastServer, slowServer);
Console.WriteLine(await completed);
// *Опционально*: здесь можно отменить еще не завершившиеся задачи через CancellationToken
}
Пример: Запустить обработку только после загрузки всех данных
В вашем приложении есть три источника данных. Только когда все три загрузились, стоит переходить к обработке:
static async Task Main()
{
var task1 = DownloadFileAsync("a.txt");
var task2 = DownloadFileAsync("b.txt");
var task3 = DownloadFileAsync("c.txt");
var all = await Task.WhenAll(task1, task2, task3);
ProcessFiles(all[0], all[1], all[2]);
}
4. Типичные ошибки при работе с Task.WhenAll и Task.WhenAny
Ошибка №1: ожидание завершения первой задачи в WhenAll.
Новички думают, что Task.WhenAll завершится, как только выполнится первая задача. На самом деле он ждёт завершения всех задач.
Ошибка №2: игнорирование исключений в WhenAll.
Если одна из задач выбросит исключение, итоговая задача будет в состоянии Faulted. Не обработав AggregateException.InnerExceptions, можно пропустить важные ошибки.
Ошибка №3: доступ к Result без проверки в WhenAny.
Если первая завершившаяся задача упала с ошибкой, вызов Result выбросит исключение. Проверяйте Task.IsFaulted перед доступом.
Ошибка №4: запуск задач последовательно вместо параллельного.
Например, ожидание каждой задачи через await в цикле вместо использования Task.WhenAll существенно снижает производительность.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ