Parallel, PLINQ, Task.WhenAll и ограничение параллелизма

↑ 2.3 Асинхронность и многопоточность · ← Предыдущая · → Следующая

Обязательно~4 мин чтенияУровень: middle

Зачем это на собесе

Часто просят распараллелить обработку списка и ограничить число одновременных запросов. Важно понимать разницу между 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.

Практика

  1. Обработать 1000 URL с лимитом 10 одновременных запросов тремя способами и сравнить.
  2. Посчитать простые числа последовательно, через Parallel.For и PLINQ, замерить.
  3. Собрать результаты WhenAll и корректно обработать частичные ошибки.

Вопросы с ответами

Связанные темы