библиотека — параллелизмlibrary — parallelism
Второй параметр стандартных алгоритмов — std::execution::seq/par/par_unseq — декларативное разрешение параллелить std::sort и десятки других алгоритмов без единой строчки ручного threading-кода.A second parameter to standard algorithms — std::execution::seq/par/par_unseq — a declarative permission to parallelize std::sort and dozens of other algorithms without a single line of manual threading code.
C++17 добавляет к большинству алгоритмов <algorithm>/<numeric> перегрузку с execution policy первым аргументом — значением одного из трёх тегов пространства имён std::execution: seq (последовательно, но явно помечено как разрешённое к пересмотру реализацией), par (разрешено выполнить на нескольких потоках) и par_unseq (разрешено параллелить и векторизовать — например, SIMD-инструкциями внутри одного потока). Сам алгоритм — тот же самый по интерфейсу и результату, меняется только то, что реализация вправе распределить работу иначе.C++17 adds to most <algorithm>/<numeric> algorithms an overload with an execution policy as the first argument — a value of one of three tags in the std::execution namespace: seq (sequential, but explicitly marked as open to reconsideration by the implementation), par (allowed to run on multiple threads), and par_unseq (allowed to parallelize and vectorize — e.g. with SIMD instructions within a single thread). The algorithm itself stays the same in interface and result — only the implementation's freedom to distribute the work differs.
const auto n = v.size();
const auto mid = v.begin() + n / 2;
std::thread t1([&] { std::sort(v.begin(), mid); });
std::thread t2([&] { std::sort(mid, v.end()); });
t1.join(); t2.join();
std::inplace_merge(v.begin(), mid, v.end());
// вручную: разбиение диапазона, join, слияние — и это только для 2 потоков// manual: range splitting, join, merge — and this is only for 2 threads
#include <execution>
std::sort(std::execution::par, v.begin(), v.end());
// та же сигнатура std::sort, но стратегию параллелизации выбирает библиотека// the same std::sort signature, but the parallelization strategy is the library's choice
Распараллеливание стандартных алгоритмов долго оставалось задачей, которую стандартная библиотека не решала сама:Parallelizing standard algorithms had long remained a task the standard library didn't solve itself:
Параллельная версия sort, reduce или for_each требовала либо подключить Intel TBB или OpenMP, либо вручную разбить диапазон на chunks, запустить std::thread на каждый и синхронизировать результат. Оба пути добавляли зависимость или code duplication на каждый алгоритм, который хотелось распараллелить.A parallel version of sort, reduce, or for_each required either pulling in Intel TBB or OpenMP, or manually splitting the range into chunks, launching a std::thread per chunk, and synchronizing the result. Both paths added a dependency or code duplication for every algorithm you wanted parallelized.
Execution policies переносят вопрос "как именно распараллелить" с программиста на реализацию STL. Программист лишь заявляет: эту операцию можно выполнять в параллель (par), можно ещё и векторизовать (par_unseq), или строго последовательно (seq) — конкретное число потоков, разбиение на chunks, выбор thread pool — детали реализации, скрытые за интерфейсом, который выглядит как обычный вызов алгоритма.Execution policies shift the question of "exactly how to parallelize" from the programmer to the STL implementation. The programmer merely declares: this operation can be run in parallel (par), can additionally be vectorized (par_unseq), or must stay strictly sequential (seq) — the exact thread count, chunk splitting, thread pool choice are implementation details hidden behind an interface that looks like an ordinary algorithm call.
Execution policy — не фича одного алгоритма, а общий второй параметр для большинства алгоритмов <algorithm> и <numeric> (sort, for_each, transform, reduce, find, count и другие) — выучив синтаксис один раз, его можно применять единообразно к любому из них.The execution policy isn't a feature of one algorithm — it's a common second parameter across most <algorithm> and <numeric> algorithms (sort, for_each, transform, reduce, find, count, and others) — learn the syntax once and apply it uniformly to any of them.
#include <execution>
#include <algorithm>
#include <numeric>
std::sort(std::execution::par, v.begin(), v.end());
// par_unseq — параллельно и векторизуемо: операция не должна// par_unseq — parallel and vectorizable: the operation must not
// иметь побочных эффектов, зависящих от порядка вызовов// have side effects that depend on call order
std::for_each(std::execution::par_unseq, v.begin(), v.end(),
[](double& x) { x = x * x; });
// reduce — параллельная версия accumulate (сумма не гарантированно// reduce — the parallel version of accumulate (the sum isn't
// вычисляется в порядке элементов — операция должна быть ассоциативна)// guaranteed to be computed in element order — the op must be associative)
double total = std::reduce(std::execution::par, v.begin(), v.end(), 0.0);
Фокус этой страницы — именно на алгоритмах. Как устроены сами примитивы синхронизации под капотом (mutex, atomic, memory order, lock-free структуры) разобрано отдельно и глубже в cpp_concurrency.md — если после execution policies возникает вопрос "а что, если операция внутри алгоритма разделяет состояние между вызовами", ответ там же: нужна та же дисциплина atomics/mutex, что и в ручном многопоточном коде, execution policy сама по себе от гонок не защищает.This page's focus is specifically on algorithms. How the synchronization primitives themselves work underneath (mutex, atomic, memory order, lock-free structures) is covered separately and in more depth in cpp_concurrency.md — if after execution policies the question arises "what if the operation inside the algorithm shares state across calls," the answer lives there too: the same atomics/mutex discipline as in manual multithreaded code is required, an execution policy by itself doesn't protect against races.
Дальнейшее развитие этой же идеи — "декларативно описать асинхронную/параллельную работу и оставить исполнение планировщику" — продолжается моделью senders/receivers, готовящейся как std::execution в C++26: это более общий и композируемый механизм, чем execution policies алгоритмов C++17, но решает родственную задачу.The further evolution of this same idea — "declaratively describe asynchronous/parallel work and leave execution to a scheduler" — continues with the senders/receivers model being prepared as std::execution for C++26: a more general and composable mechanism than C++17's algorithm execution policies, but addressing a related problem.
Реализация вправе выполнить алгоритм с par строго последовательно, если сочтёт это более эффективным для конкретного размера диапазона или платформы — стандарт не даёт способа потребовать конкретное число потоков или гарантированное распараллеливание. Ожидание "поставил par — обязательно ускорится" не всегда оправдывается: для маленьких диапазонов накладные расходы на распределение работы могут превысить выигрыш.An implementation is free to execute a par-tagged algorithm strictly sequentially if it judges that more efficient for the given range size or platform — the standard gives no way to demand a specific thread count or guaranteed parallelization. Assuming "I passed par, so it will definitely speed up" doesn't always hold: for small ranges, the overhead of distributing work can exceed the gain.
Функция/лямбда, переданная алгоритму с par или par_unseq, не должна иметь гонок данных при параллельном вызове на разных элементах — если она пишет в общее состояние без синхронизации, это undefined behavior, и компилятор об этом не предупредит и не проверит на этапе компиляции. par_unseq дополнительно запрещает операции с побочными эффектами, зависящими от порядка (в том числе владение мьютексом на время вызова — векторизация с этим несовместима).A function/lambda passed to an algorithm with par or par_unseq must not have data races when called concurrently on different elements — if it writes to shared state without synchronization, that's undefined behavior, and the compiler won't warn or check it at compile time. par_unseq additionally forbids operations with order-dependent side effects (including holding a mutex during the call — vectorization is incompatible with that).
К моменту выхода C++17 не все компиляторы/стандартные библиотеки реализовали execution policies полностью и одинаково — например, GCC исторически получал параллельные алгоритмы через зависимость от Intel TBB (без неё некоторые par-перегрузки просто не компилируются или откатываются на последовательное исполнение). Актуальный статус поддержки стоит проверять по документации конкретной версии компилятора/libstdc++/libc++, а не считать, что раз стандарт C++17, значит любой алгоритм с execution policy уже параллелен на любом тулчейне.By the time C++17 shipped, not every compiler/standard library implemented execution policies fully or identically — for example, GCC has historically required Intel TBB as a dependency for parallel algorithms (without it, some par overloads simply fail to compile or fall back to sequential execution). The current support status is worth checking against the specific compiler/libstdc++/libc++ version's documentation, rather than assuming that because the standard is C++17, any algorithm with an execution policy is already parallel on any toolchain.