C++ Multithreading Cheat Sheet
Covers C++ mutexes, lock guards, condition variables, atomics, and std::async/std::future for coordinating concurrent, thread-safe code.
Mutex & Lock Guards
Protect shared state from concurrent access with mutexes.
#include <mutex>std::mutex mtx;int counter = 0;void increment() { std::lock_guard<std::mutex> lock(mtx); // RAII: unlocks automatically ++counter;}// unique_lock allows manual unlock/relock, and is required by condition_variablestd::unique_lock<std::mutex> ulock(mtx);ulock.unlock();ulock.lock();// lock two mutexes without deadlockstd::mutex m1, m2;std::scoped_lock lock(m1, m2); // C++17, deadlock-avoiding multi-lock
Condition Variables
Block a thread until another thread signals a condition.
#include <condition_variable>std::mutex mtx;std::condition_variable cv;bool ready = false;void worker() { std::unique_lock<std::mutex> lock(mtx); cv.wait(lock, [] { return ready; }); // sleeps until predicate is true // ... do work ...}void producer() { { std::lock_guard<std::mutex> lock(mtx); ready = true; } cv.notify_one(); // wake one waiter; use notify_all() to wake all}
Atomics
Lock-free operations on shared variables for simple counters/flags.
#include <atomic>std::atomic<int> counter{0};counter++; // atomic increment, no mutex neededcounter.fetch_add(1, std::memory_order_relaxed);std::atomic<bool> flag{false};flag.store(true, std::memory_order_release);bool v = flag.load(std::memory_order_acquire);int expected = 0;counter.compare_exchange_strong(expected, 10); // CAS operation
std::async and std::future
Run a task asynchronously and retrieve its result later.
#include <future>int compute(int x) { return x * x; }std::future<int> fut = std::async(std::launch::async, compute, 5);// ... do other work ...int result = fut.get(); // blocks until the result is ready (only callable once)std::promise<int> prom;std::future<int> f2 = prom.get_future();std::thread t([&prom] { prom.set_value(42); });t.join();std::cout << f2.get();
Synchronization Primitives
Tools available in the C++ standard library for thread coordination.
- std::mutex- Basic mutual exclusion lock; non-recursive, must be unlocked by the locking thread.
- std::recursive_mutex- Same thread may lock it multiple times; must unlock the same number of times.
- std::shared_mutex- Reader-writer lock (C++17); multiple readers or one writer via lock_shared()/lock().
- std::condition_variable- Lets threads wait for a condition and be notified when it changes.
- std::atomic<T>- Lock-free (on most platforms) operations on a single variable without an explicit mutex.
- Deadlock- Two or more threads waiting on locks the others hold; avoid with consistent lock ordering or std::scoped_lock.
std::jthread & Cooperative Cancellation
C++20 self-joining thread with built-in stop_token support for clean cancellation.
#include <thread>#include <stop_token>void worker(std::stop_token tok) { while (!tok.stop_requested()) { // do incremental work }}std::jthread t(worker); // auto-joins in destructor, no manual t.join() needed// ... later, from any thread ...t.request_stop(); // sets the stop_token; worker sees stop_requested() == true// register a callback that fires when stop is requestedstd::stop_callback cb(t.get_stop_token(), [] { std::cout << "cleanup on cancellation\n";});
Explicit Memory Ordering
Fine-tune atomic synchronization semantics beyond the default sequentially-consistent order.
#include <atomic>std::atomic<bool> ready{false};std::atomic<int> payload{0};// producer threadpayload.store(42, std::memory_order_relaxed); // no ordering guarantee aloneready.store(true, std::memory_order_release); // publishes payload write// consumer threadwhile (!ready.load(std::memory_order_acquire)) {} // pairs with release aboveint v = payload.load(std::memory_order_relaxed); // guaranteed to see 42// std::memory_order_relaxed: no ordering, only atomicity (fast counters)// std::memory_order_acquire/release: pairs to establish happens-before// std::memory_order_seq_cst: default, total global order, safest but slowest
std::latch and std::barrier (C++20)
One-shot and reusable thread synchronization points for fan-in/fan-out workloads.
#include <latch>#include <barrier>// latch: wait for N events, then proceed (single use)std::latch workDone(3);for (int i = 0; i < 3; ++i) { std::thread([&] { /* work */ workDone.count_down(); }).detach();}workDone.wait(); // blocks until all 3 have counted down// barrier: repeated rendezvous point across phases, with optional completion callbackstd::barrier sync_point(4, [] { std::cout << "phase complete\n"; });void phase_worker() { // ... phase 1 work ... sync_point.arrive_and_wait(); // blocks until all 4 threads arrive // ... phase 2 work ...}
Concurrency Pitfalls & Terms
Subtle problems that surface only under real concurrent load.
- False sharing- Unrelated variables on the same cache line cause needless cache invalidation between cores; pad hot atomics to cache-line size (std::hardware_destructive_interference_size).
- ABA problem- A lock-free compare_exchange sees the same value twice but the underlying data changed in between; fix with tagged pointers or versioned counters.
- Spurious wakeup- A condition_variable::wait can return without a notify_*; always wait with a predicate, never a bare wait().
- Priority inversion- A low-priority thread holding a lock blocks a high-priority thread; priority-inheriting mutexes mitigate it on supported platforms.
- Thread-local storage (thread_local)- Gives each thread its own copy of a variable, avoiding synchronization for per-thread state like RNG engines or error buffers.
- Thundering herd- notify_all() wakes every waiter but only one can proceed; the rest re-block, wasting scheduler cycles under high contention.
- Lock striping- Splitting one big lock into many fine-grained locks (e.g. per-bucket in a hash map) to reduce contention without going fully lock-free.
- TSan (ThreadSanitizer)- Compile with -fsanitize=thread to detect data races at runtime that are otherwise invisible in testing.
Prefer std::lock_guard or std::unique_lock over manual mutex.lock()/unlock() - an exception thrown between lock and unlock would leave the mutex permanently locked, while RAII guarantees release on stack unwind.