Synchronization Primitives

Synchronization Primitives

A quick guide to mutexes, shared locks, semaphores, latches, barriers, and coordination patterns.

Synchronization Primitives

Core building blocks

Choose the primitive by the rule you need

Reader-writer locking

std::shared_mutex mutex;

void read_value() {
    std::shared_lock lock(mutex);
}

void write_value() {
    std::unique_lock lock(mutex);
}

Use this only when you have measured read-heavy contention. A plain mutex is often simpler and fast enough.

Semaphores

std::counting_semaphore<4> slots(4);

Use semaphores when access should be capped instead of fully serialized.

slots.acquire();
use_connection();
slots.release();

This pattern fits connection pools, worker slots, or rate-limited resources.

Coordination tools

Condition variable pattern

std::mutex guard;
std::condition_variable cv;
std::queue<int> work;
bool done = false;

void consumer() {
    std::unique_lock lock(guard);
    cv.wait(lock, [&] { return done || !work.empty(); });
}

Always wait with a predicate. That protects you from spurious wakeups and documents the actual condition being awaited.

Latch vs barrier

std::latch ready(3);
ready.count_down();
ready.wait();

Practical guidance

Common failure modes

Relationship to atomics

Atomics coordinate individual memory operations. Synchronization primitives coordinate larger invariants and ownership of critical sections. If your rule is about a whole data structure, a mutex is usually the right starting point.

Example in practice

#include <thread>

int main() {
    std::jthread worker([] { /* coordinate work */ });
}

Try this variation

Add a stop condition or notification path. Concurrency examples become useful only once shutdown and coordination are explicit.

#include <stop_token>
#include <thread>

int main() {
    std::jthread worker([](std::stop_token token) {
        while (!token.stop_requested()) {
            break;
        }
    });
}