C++ Concurrency And Memory Model
Asked of: Software Engineer
Last updated

What's being tested
Interviewers probe your practical mastery of the C++ memory model and concurrency primitives: safe coordination between threads, the distinction between atomic and non-atomic accesses, and how memory-ordering choices affect correctness and performance. They want evidence you can reason about data races, pick the right synchronization (locks vs lock-free), and justify tradeoffs (latency, throughput, complexity) for real code. For eBay-scale services this maps to building correct, low-latency thread-safe components (caches, queues, metrics collectors) that won't misbehave under contention.
Core knowledge
-
Data race: simultaneous conflicting accesses (at least one write) to the same scalar object without synchronization cause undefined behavior; prevent with
std::atomicor locks likestd::mutex. -
Happens-before: a directed relation guaranteeing visibility; a release store on one thread and an acquire load on another create a happens-before edge so prior writes become visible.
-
Memory orders:
std::memory_order_seq_cst,acq_rel/acquire/release, andrelaxeddefine visibility and reordering; useacquire/releasefor most producer-consumer andseq_cstonly when global ordering needed. -
Atomic operations:
std::atomic<T>::load/store/exchangeandcompare_exchange_weak/strongare the building blocks for lock-free algorithms;compare_exchange_weakmay spuriously fail and is for loops. -
Atomic fences:
std::atomic_thread_fenceenforces ordering without a particular atomic variable; use for fine-grained ordering when needed. -
Locks vs lock-free: mutex (
std::mutex) provides simplicity and composability; lock-free (using atomics) can give lower latency but adds complexity, ABA issues, and harder memory reclamation. -
ABA problem: in compare-and-swap loops a pointer can be A→B→A and fool CAS; mitigate with versioned pointers (tagged counters), hazard pointers, or epoch-based reclamation.
-
Lock-free/wait-free: lock-free guarantees system progress; wait-free guarantees per-thread progress. Most practical designs aim for lock-free; wait-free is rare and complex.
-
Lazy init idioms: prefer Meyers' singleton (function-local static) for safe lazy init in modern C++; double-checked locking must use correct atomics and
memory_orderto be safe. -
Destruction & lifetime: static/dynamic init order, destruction races, and safe reclamation are common tripwires; prefer explicit shutdown paths or shared ownership (
shared_ptr) when threads may outlive producers. -
Performance tradeoffs:
relaxedcan avoid fences for counters where only per-thread accumulation and occasional aggregation occurs; useacquire/releasefor correctness-critical synchronization. -
Debugging tips: tools like
ThreadSanitizer(TSAN) detect data races; perf counters (p99,throughput) measure contention; TSAN/UBSan are essential pre-commit checks.
Worked example — "Implement a thread-safe lazy singleton using double-checked locking in C++11"
First 30 seconds: clarify whether construction must be lazy, whether exceptions from constructor are allowed, and lifetime guarantees (program exit vs explicit destroy). State assumptions: single-instantiation globally; threads may concurrently request instance. Skeleton answer pillars: (1) prefer Meyers' singleton if allowed (function-local static), (2) if implementing DCL, use std::atomic<Singleton*> for the pointer and a std::mutex for initialization, (3) perform an initial atomic.load with memory_order_acquire, then if null lock the mutex and check again, then atomic.store with memory_order_release. Flag an explicit tradeoff: DCL is more error-prone than function-local statics and can fail if incorrect memory orders are used. Show correctness touchpoint: acquire load pairs with release store to ensure fully-constructed object is visible. Close: mention exception-safety (use std::unique_ptr during construction) and that with more time you'd explain destruction ordering, consider std::shared_ptr for controlled lifetime, or prefer the simple static approach unless lazy init control is mandatory.
A second angle — lock-free stack using atomics and ABA concerns
Frame: now constraints change — no locks allowed, push/pop must be low-latency under contention. Use std::atomic<Node*> head with compare_exchange_weak loops for push/pop. Key differences: memory reclamation becomes the hardest part — freeing a popped node can reintroduce ABA, so mention hazard pointers, epoch-based reclamation, or pointer-tagging as mitigation. Another important angle is ordering: release on push store and acquire on pop load suffice for data visibility; but some platforms may require seq_cst for correctness if relying on global ordering invariants. Explicitly call out tradeoff: lock-free yields better throughput at high concurrency but increases code complexity and maintenance cost.
Common pitfalls
Pitfall: assuming
std::atomic<T>makes composite operations atomic — reads/writes of the atomic itself are atomic, but a sequence of operations (check-then-act) requires explicit synchronization such as CAS or a mutex.
Pitfall: using
relaxedmemory order for correctness-sensitive synchronization —relaxedprovides no ordering guarantees and will cause subtle visibility bugs when used instead ofacquire/release.
Pitfall: using double-checked locking without proper memory orders — a plain load/store can reorder so another thread sees a non-null pointer before the object construction completes, leading to UB; always pair
memory_order_acquireload withmemory_order_releasestore or usestd::call_once/function-local static.
Connections
Interviewers often pivot to adjacent topics: concurrency testing and profiling (ThreadSanitizer, stress tests, contention hotspots), and memory reclamation schemes (hazard pointers, epoch/RCU). They may also connect to higher-level designs like thread pools and async patterns (std::async, std::future) or distributed concurrency concerns (sharding to reduce contention).
Further reading
-
cppreference: atomic — concise reference of
std::atomicAPIs and semantics. -
cppreference: memory_order — summary table of memory orders and guarantees.
-
Herb Sutter — Atomic Weapons — practical explanation of C++ atomic pitfalls and guidance.
Related concepts
- C++ Concurrency, Memory Ownership, And RAII
- C++ Systems, Memory, Concurrency, And VirtualizationSoftware Engineering Fundamentals
- Concurrency, Deadlocks, And SynchronizationSoftware Engineering Fundamentals
- Concurrency, OS Threads, And MemorySoftware Engineering Fundamentals
- Concurrency And SynchronizationSoftware Engineering Fundamentals
- Concurrency Control And Thread SafetySystem Design