C++ Concurrency (std::thread) Cheat Sheet
Covers creating and managing std::thread objects, passing arguments safely, join versus detach semantics, and thread lifecycle pitfalls.
Creating Threads
Spawn a thread from a function, lambda, or callable object.
#include <thread>void task(int id) { std::cout << "Thread " << id << " running\n";}std::thread t1(task, 1); // function + argsstd::thread t2([] { std::cout << "lambda\n"; }); // lambdastruct Functor { void operator()() { std::cout << "functor\n"; }};std::thread t3(Functor{});t1.join();t2.join();t3.join();
Passing Arguments
Arguments are copied into the thread by default; use std::ref for references.
void modify(int& value) { value *= 2; }int x = 10;// std::thread t(modify, x); // ERROR: would copy x, function expects int&std::thread t(modify, std::ref(x)); // pass by reference explicitlyt.join();std::cout << x; // 20// Move-only arguments must be moved instd::unique_ptr<int> p = std::make_unique<int>(5);std::thread t2([](std::unique_ptr<int> up) { std::cout << *up; }, std::move(p));t2.join();
join() vs detach()
Decide how a thread's lifetime relates to the spawning thread.
std::thread worker(longRunningTask);worker.join(); // blocks the current thread until worker finishes// ORworker.detach(); // worker runs independently; caller no longer manages it // detached threads must not access destroyed local state// Both throw std::system_error if called on a thread that's not joinableif (worker.joinable()) { worker.join();}// RAII wrapper to guarantee join on scope exit (avoids std::terminate)struct ThreadGuard { std::thread& t; ~ThreadGuard() { if (t.joinable()) t.join(); }};
Thread IDs & Hardware Concurrency
Identify threads and query available parallelism.
std::cout << std::this_thread::get_id();unsigned n = std::thread::hardware_concurrency(); // approx # of hw threads, 0 if unknownstd::this_thread::sleep_for(std::chrono::milliseconds(100));thread_local int callCount = 0; // separate instance per thread
Key Facts
Behaviors that trip up new std::thread users.
- Non-copyable- std::thread objects can be moved but not copied; ownership of the OS thread transfers on move.
- Uncaught destructor- If a joinable std::thread is destroyed without join() or detach() being called, std::terminate() is invoked.
- Arguments are copied by default- Even if the target function takes a reference, arguments are copied unless wrapped in std::ref/std::cref.
- Exceptions don't cross threads automatically- An uncaught exception in a thread function calls std::terminate(); propagate results via std::promise/std::future instead.
- std::jthread (C++20)- Auto-joins on destruction and supports cooperative cancellation via std::stop_token.
std::mutex, lock_guard & unique_lock
Protect shared state from data races using RAII lock wrappers.
#include <mutex>std::mutex m;int shared_counter = 0;void increment() { std::lock_guard<std::mutex> lock(m); // locks on construction, unlocks on scope exit ++shared_counter;} // lock released here even if an exception is thrownvoid conditional_lock() { std::unique_lock<std::mutex> lock(m, std::defer_lock); // don't lock yet // ... do work that doesn't need the mutex ... lock.lock(); // lock explicitly when needed ++shared_counter; lock.unlock(); // can unlock early, unlike lock_guard // lock re-acquired automatically at scope exit if still owned}// recursive_mutex allows the same thread to lock multiple timesstd::recursive_mutex rm;
condition_variable Producer/Consumer
Coordinate threads waiting on a shared predicate without busy-polling.
#include <condition_variable>#include <queue>std::mutex mtx;std::condition_variable cv;std::queue<int> q;bool done = false;void producer() { for (int i = 0; i < 5; ++i) { { std::lock_guard<std::mutex> lock(mtx); q.push(i); } cv.notify_one(); // wake one waiting consumer } { std::lock_guard<std::mutex> lock(mtx); done = true; } cv.notify_all();}void consumer() { std::unique_lock<std::mutex> lock(mtx); cv.wait(lock, [] { return !q.empty() || done; }); // predicate guards spurious wakeups while (!q.empty()) { std::cout << q.front() << "\n"; q.pop(); }}
std::atomic for Lock-Free Counters
Perform simple shared updates without an explicit mutex.
#include <atomic>std::atomic<int> counter{0};void worker() { for (int i = 0; i < 1000; ++i) { counter.fetch_add(1, std::memory_order_relaxed); // no ordering guarantees needed }}// compare-and-swap for lock-free algorithmsstd::atomic<bool> flag{false};bool expected = false;if (flag.compare_exchange_strong(expected, true)) { // this thread won the race, flag was false and is now true}// atomic pointer publish with acquire/release orderingstd::atomic<int*> ptr{nullptr};void publish(int* p) { ptr.store(p, std::memory_order_release); }int* consume() { return ptr.load(std::memory_order_acquire); }
std::async, std::future & std::promise
Run tasks that return values and propagate exceptions across threads.
#include <future>int compute(int x) { return x * x; }std::future<int> f = std::async(std::launch::async, compute, 6);// ... do other work ...int result = f.get(); // blocks until ready; throws if compute() threw// promise/future for manual signaling between threadsstd::promise<int> prom;std::future<int> fut = prom.get_future();std::thread producer([&prom] { try { prom.set_value(42); } catch (...) { prom.set_exception(std::current_exception()); }});std::cout << fut.get();producer.join();
Deadlock Avoidance Techniques
Strategies for preventing threads from waiting on each other forever.
- std::lock / std::scoped_lock- Locks multiple mutexes atomically using a deadlock-avoidance algorithm, preventing the classic 'lock A then B vs lock B then A' cycle.
- Consistent lock ordering- Always acquire mutexes in the same global order across all threads to eliminate circular wait conditions.
- Lock hierarchies- Assign each mutex a hierarchy level and forbid acquiring a lower-level lock while holding a higher one.
- Minimize critical sections- Hold locks for the shortest time possible; never do I/O or call unknown code while holding a mutex.
- std::try_lock- Attempts to lock without blocking; back off and retry rather than waiting indefinitely when contention is high.
- Avoid nested locks- Prefer redesigning shared state so a single mutex protects it, rather than requiring multiple locks held simultaneously.
Prefer std::jthread (C++20) over std::thread when available - it automatically joins in its destructor, eliminating the classic bug where an exception or early return skips a manual join() and crashes the program via std::terminate.