Что такое Parallel в C#?

«Что такое Parallel в C#?» — вопрос из категории Многопоточность, который задают на 28% собеседований C# Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Parallel — это класс в пространстве имён System.Threading.Tasks, предоставляющий упрощённый API для параллельного выполнения циклов (for, foreach) и независимых действий над коллекциями данных. Он абстрагирует ручное управление потоками, используя пул потоков .NET.

Основные методы:

  • Parallel.For — параллельный аналог цикла for.
  • Parallel.ForEach — параллельный аналог цикла foreach.
  • Parallel.Invoke — параллельное выполнение набора действий (Action).

Когда использовать? Идеально для CPU-bound задач, где есть большой объём однотипных вычислений, которые можно выполнять независимо (например, обработка пикселей изображения, симуляции, математические расчёты).

Пример: параллельная обработка коллекции.

using System.Threading.Tasks;

var data = Enumerable.Range(1, 10000).ToList();
var results = new double[data.Count];

// Последовательная обработка
foreach (var item in data)
{
    results[item - 1] = PerformHeavyCalculation(item);
}

// Параллельная обработка с Parallel.For
Parallel.For(0, data.Count, i =>
{
    // Каждая итерация может выполняться в отдельном потоке
    results[i] = PerformHeavyCalculation(data[i]);
});

// Параллельная обработка с Parallel.ForEach
Parallel.ForEach(data, (item, state, index) =>
{
    results[index] = PerformHeavyCalculation(item);
});

static double PerformHeavyCalculation(int input)
{
    // Имитация ресурсоёмкой операции
    return Math.Sqrt(Math.Pow(input, 3)) * Math.Tan(input);
}

Критически важные моменты:

  1. Потокобезопасность: Тело цикла (Action) выполняется в нескольких потоках одновременно. Доступ к общим ресурсам (коллекциям, статическим полям) должен быть синхронизирован (например, с помощью lock, ConcurrentBag, Interlocked).
    var sharedSum = 0;
    object lockObj = new object();
    Parallel.For(0, 1000, i =>
    {
        // НЕПРАВИЛЬНО: sharedSum += i; // Race condition!
        // ПРАВИЛЬНО:
        lock (lockObj) { sharedSum += i; }
        // ИЛИ ЛУЧШЕ (для простых операций):
        Interlocked.Add(ref sharedSum, i);
    });
  2. Порядок выполнения: Итерации выполняются не по порядку. Нельзя полагаться на последовательность.
  3. Неподходящие сценарии: Для I/O-bound операций (чтение файлов, сетевые запросы) Parallel блокирует потоки пула. Вместо этого используйте асинхронное программирование (Task.WhenAll, Parallel.ForEachAsync в .NET 6+).
  4. Накладные расходы: Для очень маленьких коллекций или простых операций оверхед на распараллеливание может превысить выгоду. Всегда замеряйте производительность.