Потоки и OpenMP: разделяемая память на директивах
Урок знакомит с самым простым способом распараллелить вычисления на многоядерном CPU.
OpenMP — стандарт для параллелизма с разделяемой памятью: программист расставляет директивы
#pragma omp, а компилятор сам создаёт и координирует потоки.
Зачем OpenMP
Писать потоки вручную (создавать, синхронизировать, делить данные) утомительно и ошибочно. OpenMP даёт декларативный подход: вы помечаете цикл директивой, говоря «итерации независимы, распараллель», и компилятор разбивает диапазон между потоками. Модель — разделяемая память: все потоки видят одни и те же переменные, поэтому коммуникация бесплатна, но нужна аккуратность с общими данными.
Ниже — типичный пример: параллельное суммирование с редукцией. Это C++ с OpenMP, исполняется на сервере с компилятором — в браузере он не запускается, поэтому помечен как нечитаемый для раннера блок (только подсветка).
#include <omp.h>
#include <vector>
#include <iostream>
int main() {
std::vector<double> a(1000000, 1.0);
double sum = 0.0;
// распараллеливаем цикл; reduction безопасно складывает частичные суммы
#pragma omp parallel for reduction(+:sum)
for (int i = 0; i < (int)a.size(); ++i) {
sum += a[i];
}
std::cout << "sum = " << sum << std::endl;
return 0;
}
Директива #pragma omp parallel for раздаёт итерации цикла потокам. Конструкция reduction(+:sum) критична: без неё потоки одновременно писали бы в общую sum — гонка данных. С reduction каждый поток копит свою частичную сумму, а в конце они складываются (это та самая параллельная редукция из раздела 4).
Ключевые директивы
| Директива | Что делает |
parallel | создаёт команду потоков |
for | делит итерации цикла между потоками |
reduction(op:var) | безопасно сворачивает переменную (сумма, макс...) |
critical | секция, которую выполняет лишь один поток за раз |
barrier | все потоки ждут друг друга в точке |
schedule(dynamic) | динамическая балансировка итераций |
Иллюстрация: что делает reduction (на Python)
Чтобы понять логику reduction без C++, эмулируем её последовательно: каждый «поток» суммирует свой кусок, потом частичные суммы складываются.
a = [1.0] * 1_000_000
threads = 4
chunk = len(a) // threads
partial = []
for t in range(threads):
partial.append(sum(a[t*chunk:(t+1)*chunk])) # частичная сумма потока
print("частичные суммы:", partial)
print("reduction(+):", sum(partial))
Вывод:
частичные суммы: [250000.0, 250000.0, 250000.0, 250000.0] reduction(+): 1000000.0
schedule: статика против динамики
OpenMP позволяет выбрать, как делить итерации. schedule(static) — поровну заранее (дёшево, хорошо при равной стоимости итераций). schedule(dynamic) — потоки забирают порции по мере освобождения (это work stealing из раздела 6, дороже, но выравнивает нагрузку при неравных итерациях). Выбор schedule — главный рычаг балансировки в OpenMP.
Как работает под капотом
OpenMP не магия: компилятор разворачивает директивы в вызовы рантайма, который держит пул потоков. parallel «будит» потоки пула, for вычисляет диапазон итераций каждого потока, барьер в конце ждёт всех. Поток-мастер продолжает после параллельной области. Поскольку память общая, переменные по умолчанию разделяемые — отсюда главный источник ошибок: случайная запись в общую переменную без reduction или critical.
Главная ценность OpenMP — низкий порог входа: можно взять готовый последовательный цикл и распараллелить его одной строкой, не переписывая программу. Это делает его идеальной отправной точкой для распараллеливания существующего кода. Но та же простота таит ловушку: добавить директиву легко, а вот убедиться, что итерации действительно независимы, — на совести программиста. Компилятор не проверяет независимость; он слепо доверяет вашему #pragma. Поэтому работа с OpenMP — это в основном работа с зависимостями: понять, какие переменные общие, какие приватные, где накопление требует reduction, а где нужен critical. Сама директива — мелочь; всё мышление в анализе данных.
Частые ошибки
- Распараллелить цикл с зависимостями между итерациями — получите неверный результат (итерации обязаны быть независимы).
- Забыть reduction для накапливаемой переменной — гонка данных, мусорная сумма.
- Злоупотреблять critical — он сериализует код и убивает параллелизм.
Итоги
- OpenMP распараллеливает циклы директивами #pragma в модели разделяемой памяти.
- reduction безопасно сворачивает накапливаемую переменную (избегает гонки).
- schedule(static/dynamic) управляет балансировкой; критично, чтобы итерации были независимы.