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

Ответ 18+ 🔞

Смотри, есть такая штука в .NET — Parallel. Это, по сути, обёртка, чтобы не ебаться с потоками вручную, когда нужно тупо нагрузить все ядра процессора какой-нибудь однотипной хуйнёй. Типа циклов for или foreach, но чтобы они бежали одновременно, а не друг за другом.

Что там есть по методам:

  • Parallel.For — это как обычный for, только параллельный.
  • Parallel.ForEach — ну, ты понял, для коллекций.
  • Parallel.Invoke — когда нужно запустить кучу независимых действий разом.

Когда это впихивать? Идеально, когда у тебя 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]);
});

// Или через 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. Потокобезопасность — это всё. Тело цикла выполняется в нескольких потоках сразу. Если лезешь без спросу в общую переменную или коллекцию — получишь race condition и неконсистентные данные. Используй lock, потокобезопасные коллекции (ConcurrentBag) или Interlocked для простых операций.
    var sharedSum = 0;
    object lockObj = new object();
    Parallel.For(0, 1000, i =>
    {
        // ТАК НЕЛЬЗЯ: sharedSum += i; // Будет полная хуйня, данные потекут!
        // ТАК НАДО:
        lock (lockObj) { sharedSum += i; }
        // ИЛИ ТАК (для атомарных операций):
        Interlocked.Add(ref sharedSum, i);
    });
  2. Порядок — не гарантирован. Итерации начнут и закончат выполняться в произвольном порядке. Не строй логику, которая от этого зависит.
  3. Не для всего подходит. Если твоя задача — качать файлы из интернета или читать с диска (I/O-bound), то Parallel будет создавать кучу потоков, которые просто будут висеть и ждать. Это тупо и неэффективно. Для такого есть асинхронщина (Task.WhenAll, Parallel.ForEachAsync в .NET 6+).
  4. Накладные расходы. Если твоя коллекция — три элемента, а операция внутри — сложение двух чисел, то оверхед на распараллеливание сожрёт всю выгоду. Для мелких задач это пустая трата времени. Всегда проверяй производительность, не верь на слово.