Підрозділ 16.4
Клас Parallel
16.4. Клас Parallel Клас System.Threading.Tasks.Parallel надає зручні статичні методи для паралельного виконання операцій, не вимагаючи явного створення і управління завданнями. Він автоматично розподіляє робот
16.4. Клас Parallel
Клас System.Threading.Tasks.Parallel надає зручні статичні методи для паралельного виконання операцій, не вимагаючи явного створення і управління завданнями. Він автоматично розподіляє роботу між доступними ядрами процесора і чекає завершення всіх паралельних операцій перед поверненням.
На відміну від Task.Run(), де розробник явно описує кожне завдання, Parallel орієнтований на масові паралельні операції: виконання набору незалежних дій, паралельний обхід колекцій, паралельний цикл. Він ефективний, коли є багато однотипних незалежних завдань, що добре розпаралелюються.
Parallel.Invoke — паралельне виконання дій
Parallel.Invoke приймає довільний набір делегатів Action і виконує їх паралельно. Метод повертається тільки після завершення всіх переданих дій:
Parallel.Invoke не гарантує порядку виконання і не гарантує, що кожна дія виконається в окремому потоці — якщо ядер менше, ніж дій, деякі дії будуть почергувати. Але результат завжди правильний: метод поверне управління тільки після завершення всіх.
Parallel.For — паралельний цикл for
Parallel.For(fromInclusive, toExclusive, body) виконує тіло циклу паралельно для всіх індексів у діапазоні [from, to). Планувальник TPL автоматично розбиває діапазон на пакети і виконує їх у потоках пулу:
Зверніть: в одній ітерації використовується Thread.CurrentThread.ManagedThreadId — він показує, що різні ітерації можуть виконуватись у різних потоках пулу.
Parallel.ForEach — паралельний обхід колекції
Parallel.ForEach — аналог Parallel.For, але замість індексів він приймає колекцію IEnumerable<T> і виконує тіло для кожного елемента паралельно:
Метод повертає структуру ParallelLoopResult з двома властивостями:
IsCompleted—true, якщо цикл пройшов до кінця без перериваньLowestBreakIteration— індекс, на якому відбулось переривання черезBreak(), абоnull

ParallelLoopState: керування циклом
Звичайний break у тілі Parallel.For викличе помилку компілятора — ви не можете використовувати break у лямбді. Натомість Parallel надає об'єкт ParallelLoopState, який передається як другий параметр:
Break() — зупинка після поточного діапазону
pls.Break() сигналізує планувальнику, що після завершення всіх ітерацій з індексом, меншим за поточний, подальші ітерації не потрібні. Це не негайна зупинка — ітерації з вищими індексами, що вже запущені, продовжать виконуватись:
Stop() — негайна зупинка
pls.Stop() вимагає припинити весь цикл якнайшвидше — навіть вже запущені ітерації повинні перевіряти pls.IsStopped і завершуватись достроково. Після Stop() LowestBreakIteration завжди null, а IsCompleted — false:
Break vs Stop: Break гарантує, що всі ітерації до поточної будуть виконані (корисно для пошуку з гарантованим покриттям). Stop зупиняє якнайшвидше без гарантій покриття (корисно для раннього виходу при знайденні умови).
ParallelOptions: налаштування паралелізму
ParallelOptions дозволяє налаштувати поведінку Parallel.For/ForEach/Invoke. Найважливіша властивість — MaxDegreeOfParallelism: максимальна кількість одночасно виконуваних ітерацій.
MaxDegreeOfParallelism = 1 фактично перетворює паралельний цикл на послідовний — корисно для налагодження. MaxDegreeOfParallelism = -1 (значення за замовчуванням) означає «без обмежень» — TPL сам вирішує.
У реальних системах обмеження паралелізму важливе: якщо кожен потік відкриває з'єднання з базою даних або відправляє мережевий запит, необмежений паралелізм може перевантажити зовнішній ресурс.
Коли Parallel не прискорює
Parallel — не срібна куля. Є сценарії, де паралелізація не допомагає або навіть шкодить:
Занадто короткі ітерації. Якщо тіло циклу займає мікросекунди, накладні витрати на планування задач у TPL (теж мікросекунди) можуть перевищити виграш від паралелізму. Parallel.For є ефективним, коли кожна ітерація займає щонайменше кілька мілісекунд.
Операції із спільним станом без синхронізації. Якщо всі ітерації змінюють одну змінну без lock, виникне race condition. Якщо додати lock — паралелізм де-факто зникне через серіалізацію доступу.
Послідовна природа даних. Якщо кожна ітерація залежить від результату попередньої, паралелізація неможлива — задачу потрібно переосмислити.
Interlocked.Increment — атомарна операція збільшення цілого числа, яка не потребує lock. Для простих лічильників у паралельних циклах це ефективніше за lock (obj) { count++; }.