Synchronization Primitives
Synchronization Primitives
Coordinate threads with the right primitive instead of forcing every problem into a single mutex.
Synchronization Primitives
Coordinate threads with the right primitive instead of forcing every problem into a single mutex.
Before choosing a primitive, decide what you are protecting:
The primitive should match the coordination problem.
If the rule is unclear, the primitive choice will be unclear too. Start by writing one sentence such as "only one thread may mutate the cache at a time" or "at most eight workers may use the pool concurrently."
std::shared_mutexUse this when reads are frequent and writes are rare.
std::shared_mutex cache_mutex;
std::unordered_map<std::string, int> cache;
int lookup(std::string_view key) {
std::shared_lock lock(cache_mutex);
return cache.at(std::string{key});
}
void update(std::string key, int value) {
std::unique_lock lock(cache_mutex);
cache[std::move(key)] = value;
}
This is useful for read-mostly structures, but it is not automatically faster than std::mutex. Measure contention before paying the extra complexity cost.
Use a semaphore when several tasks may proceed concurrently, but only up to a fixed limit.
std::counting_semaphore<8> db_slots(8);
This is often a better fit than a mutex when you are modeling capacity instead of exclusive ownership.
void use_slot() {
db_slots.acquire();
do_query();
db_slots.release();
}
std::latch: one-time coordination, such as "all workers are initialized".std::barrier: repeated step-by-step synchronization across iterations.std::latch ready(3);
void worker() {
prepare();
ready.count_down();
ready.wait();
run();
}
Use std::barrier instead when the threads must meet again after each phase, not just once at startup.
Condition variables remain the right tool when threads must sleep until a state predicate becomes true.
cv.wait(lock, [] { return ready; });
Condition variables remain the standard tool for "sleep until this state becomes true". Always make the state explicit and keep the wait predicate next to it.
mutex: exclusive access to a shared invariant.shared_mutex: mostly reads, rare writes, measured contention.condition_variable: sleep until a predicate becomes true.counting_semaphore: cap concurrent access to a resource pool.latch: one-time rendezvous.barrier: reusable phased rendezvous.std::mutex queue_mutex;
std::condition_variable queue_cv;
std::queue<int> jobs;
std::counting_semaphore<2> worker_slots(2);
std::barrier phase_done(3);
bool done = false;
void worker() {
while (true) {
worker_slots.acquire();
std::unique_lock lock(queue_mutex);
queue_cv.wait(lock, [&] { return done || !jobs.empty(); });
if (done && jobs.empty()) {
worker_slots.release();
break;
}
int job = jobs.front();
jobs.pop();
lock.unlock();
process(job);
worker_slots.release();
phase_done.arrive_and_wait();
}
}
This example is intentionally small, but it shows how the primitives differ:
condition_variable waits for work to appearcounting_semaphore caps concurrent active workersbarrier marks the end of a processing phaseMost real systems do not need all three at once. The point is to match each primitive to one rule instead of trying to force one primitive to model everything.
#include <thread>
int main() {
std::jthread worker([] { /* do work */ });
return 0;
}
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;
}
});
}