многопоточностьconcurrency

std::barrier / std::latch / std::counting_semaphore

Три готовых примитива синхронизации, стандартизированных в C++20: latch — одноразовый счётчик обратного отсчёта, barrier — то же самое, но переиспользуемое, semaphore — классический acquire/release-примитив, которого не было в стандартной библиотеке C++ вплоть до этого момента.Three ready-made synchronization primitives standardized in C++20: latch — a one-shot countdown counter, barrier — the same idea, but reusable, semaphore — a classic acquire/release primitive that hadn't existed in the C++ standard library until this point.

родом из Concurrency TSoriginating from the Concurrency TS latch нельзя переиспользоватьlatch cannot be reused частый вопрос: чем barrier отличается от latchcommon question: how barrier differs from latch

Что этоWhat it is

std::latch (<latch>) — одноразовый счётчик обратного отсчёта: создаётся с числом N, каждый поток вызывает count_down() по достижении точки, а wait() блокирует, пока счётчик не дойдёт до нуля. После срабатывания latch не переиспользуется. std::barrier (<barrier>) решает ту же задачу "рандеву N потоков", но циклически: после того как все N потоков вызвали arrive_and_wait(), барьер автоматически сбрасывается и готов к следующей фазе — плюс опциональный completion callback, вызываемый одним из потоков на каждой фазе. std::counting_semaphore<N>/std::binary_semaphore (<semaphore>) — классический семафор: acquire()/release(), ограничение на N одновременных "держателей" ресурса.std::latch (<latch>) is a one-shot countdown counter: created with a count N, every thread calls count_down() on reaching the point, and wait() blocks until the counter reaches zero. Once it fires, a latch isn't reused. std::barrier (<barrier>) solves the same "N threads rendezvous" problem, but cyclically: once all N threads have called arrive_and_wait(), the barrier automatically resets and is ready for the next phase — plus an optional completion callback, invoked by one of the threads on each phase. std::counting_semaphore<N>/std::binary_semaphore (<semaphore>) is a classic semaphore: acquire()/release(), capping how many "holders" of a resource can exist at once.

Происхождение: latch и barrier родом из Concurrency TS — экспериментального документа комитета, где они обкатывались до включения в основной стандарт (см. cpp_ts_extensions.md для полного разбора TS-механизма и того, что из Concurrency TS дошло до C++20, а что нет). Семафора в самом TS не было в этом виде — он стандартизирован в C++20 напрямую, закрывая давний пробел: до этого в стандартной библиотеке C++ не было счётного семафора вовсе.Origin: latch and barrier come from the Concurrency TS — an experimental committee document where they were trialed before making it into the core standard (see cpp_ts_extensions.md for the full breakdown of the TS mechanism, and what from the Concurrency TS did and didn't make it into C++20). The semaphore wasn't in the TS in this form — it was standardized directly in C++20, closing a long-standing gap: before this, the C++ standard library had no counting semaphore at all.

до C++20 — своя condition_variable + счётчикbefore C++20 — a hand-rolled condition_variable + counter
std::mutex m;
std::condition_variable cv;
int arrived = 0;
const int N = 4;

void rendezvous() {
    std::unique_lock<std::mutex> lk(m);
    ++arrived;
    if (arrived == N) cv.notify_all();
    else cv.wait(lk, [&] { return arrived == N; });
    // вручную, легко ошибиться со сбросом счётчика для след. фазы// hand-rolled, easy to get the counter reset wrong for the next phase
}
C++20 — std::barrier
std::barrier sync_point(4);   // 4 потока// 4 threads

void rendezvous() {
    sync_point.arrive_and_wait();
    // готово к следующей фазе автоматически// automatically ready for the next phase
}

Зачем ввелиWhy it was introduced

01

"N потоков должны собраться в точке" — частый паттерн без стандартного имени"N threads must rendezvous" — a common pattern with no standard name

Parallel reduce, барьерная синхронизация фаз в численных вычислениях — паттерн встречается постоянно, но до C++20 каждый раз реализовывался вручную через condition_variable + atomic-счётчик (см. пример выше). Это соседняя, но другая задача по сравнению с bounded queue из cpp11/mutex-condvar.html — там producer/consumer передают данные, здесь потокам не нужно ничего передавать друг другу, только синхронно "встретиться" в точке.Parallel reduce, phase-barrier synchronization in numerical computation — this pattern comes up constantly, but before C++20 it was hand-implemented every time via condition_variable + an atomic counter (see the example above). This is a neighboring but distinct problem compared to the bounded queue in cpp11/mutex-condvar.html — there, producer/consumer are passing data to each other; here, threads don't need to pass anything to each other, just synchronously "meet" at a point.

02

Семафора не было в стандартной библиотеке C++ вплоть до C++20The standard library had no semaphore until C++20

Семафор — классический примитив ОС (POSIX sem_t, WinAPI Semaphore), присутствующий в системном программировании десятилетиями. До C++20 использовать его из портируемого C++ кода означало либо обращаться к платформенному API напрямую (теряя переносимость), либо эмулировать семафорную семантику через mutex + condition_variable вручную — рабочий, но лишний слой абстракции для настолько базового примитива.A semaphore is a classic OS primitive (POSIX sem_t, WinAPI Semaphore), present in systems programming for decades. Before C++20, using one from portable C++ code meant either calling the platform API directly (losing portability) or emulating semaphore semantics with mutex + condition_variable by hand — workable, but an unnecessary layer of abstraction for such a basic primitive.

Как это работает под капотомHow it works under the hood

#include <latch>
#include <barrier>
#include <semaphore>

// latch — одноразово: N потоков должны финишировать перед общим стартом// latch — one-shot: N threads must finish before a shared start
std::latch workers_ready(4);
// каждый воркер: workers_ready.count_down();// each worker: workers_ready.count_down();
// главный поток: workers_ready.wait();  // once — второй раз так не заведётся// main thread: workers_ready.wait();  // once — won't fire a second time

// barrier — переиспользуемо: цикл из фаз вычисления// barrier — reusable: a loop of computation phases
std::barrier sync_phase(4, [] { /* completion callback — один поток, один раз за фазу *//* completion callback — one thread, once per phase */ });
for (int phase = 0; phase < 10; ++phase) {
    // ... вычисления фазы ...// ... phase computation ...
    sync_phase.arrive_and_wait();   // готов к следующей фазе после этого вызова// ready for the next phase after this call
}

// semaphore — ограничить доступ к ресурсу N держателями одновременно// semaphore — cap concurrent access to a resource at N holders
std::counting_semaphore<3> pool_slots(3);   // не более 3 одновременно// no more than 3 at once
pool_slots.acquire();   // заблокируется, если все 3 слота заняты// blocks if all 3 slots are taken
// ... работа с ограниченным ресурсом ...// ... work using the limited resource ...
pool_slots.release();

Все три построены поверх того же фундамента, что mutex/condition_variable/atomic (futex на Linux в неконтендед-случае, разбор — в cpp_concurrency.md), но с более узким и явно поименованным контрактом — компилятору/читателю кода не нужно распознавать паттерн "condition_variable + счётчик" по смыслу, имя типа уже говорит, что это за паттерн.All three are built on the same foundation as mutex/condition_variable/atomic (a futex on Linux in the uncontended case, covered in cpp_concurrency.md), but with a narrower, explicitly named contract — the compiler or the reader of the code doesn't need to recognize the "condition_variable + counter" pattern by inference; the type name already states what pattern this is.

std::counting_semaphore<N> закрывает ещё одну частую задачу — ограничение параллелизма доступа к ограниченному ресурсу (пул соединений, лимит одновременных worker-задач), классический паттерн "resource pool" через семафор:std::counting_semaphore<N> also covers another common problem — limiting concurrent access to a bounded resource (a connection pool, a cap on simultaneous worker tasks), the classic "resource pool" pattern implemented via a semaphore:

#include <semaphore>

class ConnectionPool {
    std::counting_semaphore<8> slots_{8};   // не более 8 одновременных соединений// no more than 8 concurrent connections
    std::vector<Connection> connections_;
public:
    Connection& acquire() {
        slots_.acquire();   // заблокирует вызывающий поток, если все 8 заняты// blocks the calling thread if all 8 are taken
        return pick_free_connection();
    }
    void release(Connection& c) {
        mark_free(c);
        slots_.release();   // освобождает слот, будит один из ожидающих acquire()// frees a slot, wakes one of the threads waiting on acquire()
    }
};   // девятый одновременный запрос просто ждёт на acquire(), пока кто-то не освободит слот// a ninth concurrent request simply waits on acquire() until someone frees a slot

Примеры из реального кодаReal-world examples

типичные сценарии использованияtypical usage scenarios
01

std::latch: ожидание инициализации N воркеров перед стартом основной работыstd::latch: waiting for N workers to initialize before starting the main work

Главный поток не должен начинать раздавать задачи, пока все воркеры не прогрели свои ресурсы (подключения, буферы) — latch создаётся один раз и срабатывает ровно один раз, что идеально подходит для одноразового барьера "все готовы к старту".The main thread shouldn't start dispatching tasks until every worker has warmed up its resources (connections, buffers) — a latch is created once and fires exactly once, which is a perfect fit for a one-shot "everyone's ready to start" barrier.

std::latch workers_ready(4);
std::vector<std::jthread> workers;

for (int i = 0; i < 4; ++i) {
    workers.emplace_back([&] {
        warm_up_resources();
        workers_ready.count_down();   // сообщить, что этот воркер готов// signal that this worker is ready
        run_worker_loop();
    });
}

workers_ready.wait();   // главный поток блокируется, пока не готовы все 4// the main thread blocks until all 4 are ready
start_dispatching_tasks();
02

std::barrier: многофазное параллельное вычисление (раунды map-reduce)std::barrier: a multi-phase parallel computation (map-reduce rounds)

Несколько раундов вычисления, где каждый следующий раунд требует, чтобы все потоки завершили предыдущий — например, параллельная редукция дерева, где на каждом уровне число активных элементов уменьшается вдвое, но все потоки должны синхронно перейти на следующий уровень одновременно.Several rounds of computation where each next round requires every thread to have finished the previous one — for example, a parallel tree reduction where the number of active elements halves at each level, but all threads must synchronously move to the next level together.

std::barrier round_sync(4);

void reduce_worker(int id, std::span<double> data) {
    for (std::size_t stride = data.size() / 2; stride > 0; stride /= 2) {
        if (static_cast<std::size_t>(id) < stride)
            data[id] += data[id + stride];
        round_sync.arrive_and_wait();   // все должны закончить текущий stride перед следующим// everyone must finish the current stride before the next one
    }
}
03

std::counting_semaphore как ограничитель параллелизмаstd::counting_semaphore as a concurrency limiter

Постановка N задач в очередь исполнения, но не более K одновременно активных — например, ограничение числа параллельных HTTP-запросов к внешнему API, чтобы не превысить rate limit, независимо от того, сколько задач вообще запланировано.Queuing up N tasks for execution, but never running more than K at the same time — for example, capping the number of parallel HTTP requests to an external API so as not to exceed a rate limit, regardless of how many tasks are scheduled in total.

std::counting_semaphore<4> concurrency_limit(4);   // не более 4 запросов одновременно// no more than 4 requests at once

void fetch_one(std::string url) {
    concurrency_limit.acquire();   // ждёт, если уже 4 запроса в полёте// waits if 4 requests are already in flight
    http_get(url);
    concurrency_limit.release();
}

// 100 URL можно запустить как 100 задач — реально работать будут максимум 4 одновременно// 100 URLs can be launched as 100 tasks — at most 4 will actually run at once
for (auto& url : urls) std::jthread(fetch_one, url).detach();
04

std::binary_semaphore как сигнал готовности между producer и consumerstd::binary_semaphore as a readiness signal between producer and consumer

Для простого случая "один producer разбудил один consumer один раз" binary_semaphore — более лёгкая альтернатива связке mutex + condition_variable: не нужен ни мьютекс, ни предикат, только acquire()/release().For the simple case of "one producer wakes one consumer once," a binary_semaphore is a lighter alternative to the mutex + condition_variable pair — no mutex or predicate needed, just acquire()/release().

std::binary_semaphore data_ready(0);   // изначально "не сигнализирован"// initially "not signaled"
std::string payload;

void producer() {
    payload = compute_result();
    data_ready.release();   // разбудить consumer — без мьютекса и без cv.notify// wake the consumer — no mutex, no cv.notify
}

void consumer() {
    data_ready.acquire();   // блокируется до единственного release() от producer// blocks until the single release() from the producer
    use(payload);
}

ГраблиGotchas

std::latch нельзя переиспользовать — для повторяющихся фаз нужен barrierstd::latch cannot be reused — repeating phases need a barrier

Попытка "сбросить" уже сработавший latch не предусмотрена API вообще — нет метода reset(). Если нужна повторяющаяся точка синхронизации (например, цикл фаз вычисления, где на каждой фазе все потоки снова должны собраться), правильный выбор — именно std::barrier, а не создание нового latch на каждую фазу вручную (что тоже работает, но избыточно и менее явно выражает намерение).There's no API support at all for "resetting" a latch that has already fired — no reset() method exists. If a repeating synchronization point is needed (say, a loop of computation phases where all threads must regroup on every phase), the right choice is specifically std::barrier, not manually constructing a new latch per phase (which also works, but is redundant and expresses intent less clearly).

Completion callback barrier исполняется ровно одним потоком, в неопределённый моментA barrier's completion callback runs on exactly one thread, at an unspecified moment

Callback вызывается ровно одним из N потоков (не всеми) где-то между завершением фазы всеми участниками и разблокировкой остальных из arrive_and_wait(). Код внутри callback не должен предполагать, что он видит какое-то конкретное согласованное с другими потоками состояние без дополнительной синхронизации — единственная гарантия в том, что он вызывается ровно один раз за фазу, а не в том, какой поток это будет или что именно уже видно/не видно другим потокам в этот момент.The callback is invoked by exactly one of the N threads (not all of them), sometime between all participants finishing the phase and the rest being released from arrive_and_wait(). Code inside the callback must not assume it sees any particular state that's consistent with the other threads without additional synchronization — the only guarantee is that it runs exactly once per phase, not which thread runs it, or exactly what is or isn't visible to the other threads at that moment.

Semaphore не имеет понятия владельца — не замена mutex по семантикеA semaphore has no notion of ownership — it's not a semantic replacement for a mutex

std::counting_semaphore не даёт информации о том, какой именно поток держит "слот" — в отличие от mutex, у которого есть понятие владельца (владеющий поток обязан сам его же и разблокировать). Семафор годится для сигнализации о доступности ресурса (в том числе между потоками, ни один из которых не является "владельцем" в классическом смысле), но не для взаимного исключения в духе mutex. std::binary_semaphore иногда используют как замену mutex (acquire/release из разных потоков разрешены — в отличие от recursive-подобных ограничений mutex), но формально семантика владения у него отличается, и полагаться на неё как на mutex — источник путаницы при код-ревью.std::counting_semaphore gives no information about which thread holds a given "slot" — unlike a mutex, which has a notion of ownership (the owning thread is required to be the one that unlocks it). A semaphore is well suited for signaling resource availability (including between threads, none of which is an "owner" in the classic sense), but not for mutex-style mutual exclusion. std::binary_semaphore is sometimes used as a mutex substitute (acquire/release from different threads is allowed — unlike mutex's recursive-like restrictions), but its ownership semantics formally differ, and relying on it as if it were a mutex is a source of confusion in code review.