Skip to content
C++

C++ Concurrency Cheat Sheet

Quick reference for std::thread, mutex, atomic, condition_variable, and async in C++.

Thread lifecycle

cpp
#include <thread>

// Launch
std::thread t(func, arg1, arg2);

// Wait for completion
t.join();

// Detach (runs independently — beware dangling captures!)
t.detach();

// Check if joinable
if (t.joinable()) t.join();

// C++20: auto-joins on destruction
std::jthread jt(func);  // no need to call join()

Mutual exclusion

cpp
#include <mutex>

std::mutex mtx;

// RAII locks
std::lock_guard<std::mutex> lg(mtx);     // lock on construction, unlock on destruction
std::unique_lock<std::mutex> ul(mtx);    // same but can unlock() / relock()
std::scoped_lock sl(mtx1, mtx2);         // C++17: lock multiple, no deadlock

// Manual (avoid if possible)
mtx.lock();
mtx.try_lock();     // returns false if already locked
mtx.unlock();

// Mutex types
std::mutex
std::recursive_mutex          // same thread can re-lock
std::timed_mutex              // try_lock_for, try_lock_until
std::shared_mutex             // C++17: reader-writer
std::shared_lock<std::shared_mutex> sl(sm);  // reader lock

Atomics

cpp
#include <atomic>

std::atomic<int> n{0};

n.load();                        // read
n.store(42);                     // write
n.exchange(10);                  // write, return old
n.fetch_add(1);   ++n;           // increment (returns old)
n.fetch_sub(1);   --n;           // decrement
n.fetch_and(mask);               // bitwise AND
n.fetch_or(mask);                // bitwise OR

// Compare-and-swap
int expected = 5;
n.compare_exchange_strong(expected, 6);
// true if swapped, false + expected=current if not

// Memory order (default: seq_cst)
n.store(1, std::memory_order_release);
n.load(std::memory_order_acquire);
n.fetch_add(1, std::memory_order_relaxed);  // no sync

// Flag (lock-free guaranteed)
std::atomic_flag flag = ATOMIC_FLAG_INIT;
flag.test_and_set();  // set, return old value
flag.clear();

Condition variable

cpp
#include <condition_variable>

std::mutex mtx;
std::condition_variable cv;
bool ready = false;

// Waiter
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return ready; });  // always use predicate!
// Process...

// Notifier
{
    std::lock_guard lock(mtx);
    ready = true;
}
cv.notify_one();   // wake one waiter
cv.notify_all();   // wake all waiters

Futures and async

cpp
#include <future>

// Launch async task
std::future<int> f = std::async(std::launch::async, []{ return 42; });
int result = f.get();  // blocks until ready

// Promise — set result from another thread
std::promise<int> p;
std::future<int> f = p.get_future();
std::thread([&p]{ p.set_value(100); }).detach();
f.get();  // 100

// Shared future — multiple readers
std::shared_future<int> sf = f.share();

// Packaged task
std::packaged_task<int(int)> task([](int n){ return n * 2; });
auto fut = task.get_future();
std::thread(std::move(task), 21).detach();
fut.get();  // 42

Call once

cpp
std::once_flag flag;
std::call_once(flag, []{ /* runs exactly once */ });

Thread-local storage

cpp
thread_local int per_thread_counter = 0;
// Each thread gets its own copy

Common patterns

cpp
// Producer-consumer queue
std::queue<Work> q;
std::mutex m;
std::condition_variable cv;

// Producer
{ std::lock_guard lock(m); q.push(item); }
cv.notify_one();

// Consumer
std::unique_lock lock(m);
cv.wait(lock, [&]{ return !q.empty(); });
auto item = q.front(); q.pop();

// Thread pool (simple)
std::vector<std::jthread> pool;
for (int i = 0; i < N; ++i)
    pool.emplace_back([&queue, &cv, &m]{ /* worker loop */ });

Memory order quick guide

OrderUse case
relaxedCounters where ordering doesn't matter
acquireLoad that starts a critical section
releaseStore that ends a critical section
acq_relRMW (fetch_add) in the middle
seq_cstDefault; full ordering (safest, slowest)
Edit on GitHub