Multithreading — a standard memory model and portable primitives
CPUs stopped getting faster around 2005 — they started getting wider instead. Modern hardware gives you 8, 16, or 64 cores, but only if your code knows how to use them. Multithreading in C++ is not just a library feature; it lives at the intersection of the language, the compiler, and the CPU. C++11 was the turning point — it gave the language a formal memory model and a portable threading library (<thread>, <mutex>, <atomic>, <condition_variable>, <future>). Before it, every concurrent C++ program had to use platform-specific APIs.
The rule everything else rests on: concurrent access by two threads to the same variable, where at least one of them writes, without synchronization, is a data race — and a data race is Undefined Behavior. Not "wrong number", UB. The compiler and the CPU are allowed to reorder operations, keep values in registers, and coalesce writes. So "it usually computes the right answer" proves nothing.
Distinguish concurrency (multiple tasks make progress without blocking each other) from parallelism (tasks actually run at the same time on different cores). C++ gives you both, but only if you spell out the visibility ordering yourself — through a mutex (sequenced-before inside the critical section) or memory_order on atomics. The full map lives in the layers below.
Topic map
- Threads —
std::thread,std::jthread,join/detach, passing arguments, thread stack,thread_local. - Synchronization —
mutex,lock_guard/unique_lock/scoped_lock,shared_mutex,condition_variable, deadlock,latch/barrier/semaphorefrom C++20. - Atomics and memory_order —
std::atomic,compare_exchange, ABA, the sixmemory_ordervalues,atomic_flag, when not to write lock-free. - future, promise, and async —
std::future/promise/packaged_task/async,shared_future, propagating exceptions between threads. - Time and waiting —
std::chrono,steady_clockvssystem_clock,sleep_for/sleep_until, timeouts in synchronization.
Common traps
| Mistake | Consequence |
|---|---|
| Accessing shared data without synchronization | Data race → UB; the compiler may drop "redundant" loads/stores |
Destroying a joinable std::thread without join()/detach() | std::terminate() in the thread destructor |
cv.wait(lk) without a predicate | Spurious wakeup → code runs while the condition is still false |
std::async without std::launch::async | May run synchronously in f.get() — no parallelism |
Treating volatile as atomic | Races remain; volatile is about MMIO/signals, not threads |
| Acquiring multiple mutexes in different orders | Deadlock; use std::scoped_lock or a global lock order |
memory_order_relaxed to publish a pointer | Reader sees the pointer before the pointee — UB |
detach() a thread that references parent's locals | Dangling refs in the frame after the parent returns |
shared_ptr<T> racing without std::atomic<shared_ptr<T>> | Control-block race; C++20 added atomic overloads |
Using system_clock to measure intervals | The wall clock can jump (NTP, user change); use steady_clock |
Interview relevance
Multithreading is a mandatory block for middle and senior interviews. The check is not "do you know std::thread" but "do you understand the memory model, UB, and happens-before."
Typical checks:
- What a data race is and why it is UB, not just "wrong number".
- Difference between
lock_guard,unique_lock, andscoped_lock— when to use which. - Why
condition_variable::waittakes a predicate (spurious wakeup, lost notification). - What happens to a joinable
std::threadin its destructor. - Concurrency vs parallelism.
- How
volatilediffers fromstd::atomic. memory_order_release/acquireand happens-before — for senior candidates.- False sharing and its performance impact.
Common wrong answer: "I'll mark it volatile and it'll be thread-safe." That is exactly the opening to explain that volatile only prevents the compiler from removing or reordering accesses inside one thread — it says nothing about visibility between threads. Thread synchronization needs std::atomic or a mutex.
Popular tasks: "write a thread-safe queue" (mutex + condition_variable + predicate); "implement a spinlock on atomic_flag"; "explain what compare_exchange_weak does in a retry loop and why weak".