Parallel, PLINQ, Task.WhenAll и ограничение параллелизма
↑ 2.3 Асинхронность и многопоточность · ← Предыдущая · → Следующая
Зачем это на собесе
Часто просят распараллелить обработку списка и ограничить число одновременных запросов. Важно понимать разницу между CPU-bound и I/O-bound.
Объяснение
| Задача | Инструмент |
|---|---|
| CPU-bound над коллекцией | Parallel.For/ForEach, PLINQ |
| I/O-bound (HTTP, БД) | Task.WhenAll, Parallel.ForEachAsync |
| Ограничить одновременность | SemaphoreSlim, MaxDegreeOfParallelism, Channel |
Parallel и PLINQ
Parallel.For(0, images.Length, i => Resize(images[i]));
var sum = numbers.AsParallel().Where(IsPrime).Sum();Они делят работу между потоками ThreadPool и подходят для вычислений. Не применяйте их для I/O: потоки будут заблокированы впустую.
Task.WhenAll
var tasks = urls.Select(u => http.GetStringAsync(u));
string[] pages = await Task.WhenAll(tasks);WhenAll ждёт все задачи. При ошибке await пробросит только первое исключение, остальные лежат в task.Exception.InnerExceptions.
Parallel.ForEachAsync (.NET 6+)
await Parallel.ForEachAsync(urls,
new ParallelOptions { MaxDegreeOfParallelism = 8, CancellationToken = ct },
async (url, token) =>
{
var html = await http.GetStringAsync(url, token);
await store.SaveAsync(url, html, token);
});Ручное ограничение
var gate = new SemaphoreSlim(5);
var tasks = urls.Select(async u =>
{
await gate.WaitAsync(ct);
try { return await http.GetStringAsync(u, ct); }
finally { gate.Release(); }
});
var results = await Task.WhenAll(tasks);Нюансы и подводные камни
- Запуск 10 000 задач без ограничения «положит» внешний сервис или исчерпает соединения; всегда ограничивайте.
Parallel.ForEachс async-лямбдой — ошибка: лямбда становитсяasync void, вызов не ждёт задач. ИспользуйтеForEachAsync.- PLINQ не гарантирует порядок без
AsOrdered(). - Общее состояние в параллельных циклах — источник гонок; агрегируйте локально и объединяйте в конце (
localInit/localFinally). - Число потоков ThreadPool ограничено: большое число блокирующих задач вызывает thread starvation.
Практика
- Обработать 1000 URL с лимитом 10 одновременных запросов тремя способами и сравнить.
- Посчитать простые числа последовательно, через
Parallel.Forи PLINQ, замерить. - Собрать результаты
WhenAllи корректно обработать частичные ошибки.
Вопросы с ответами
Когда Parallel, а когда Task.WhenAll?
Parallel— для CPU-bound, он занимает потоки.Task.WhenAll— для I/O-bound, потоки не блокируются на ожидании.
Как ограничить число одновременных async-операций?
SemaphoreSlim,Parallel.ForEachAsyncсMaxDegreeOfParallelism, либоChannelс фиксированным числом консюмеров.
Что случится, если одна из задач в WhenAll упадёт?
WhenAllдождётся всех задач;awaitвыбросит первое исключение, остальные доступны черезException.InnerExceptionsу самой задачиWhenAll.