Interview concept

C++ Concurrency And Memory Model

Asked of: Software Engineer

Last updated

Top-to-bottom decision flowchart for choosing C++ synchronization and memory orders: start question, diamonds for decisions (single object? low latency? ordering?), branches to use std::mutex, std::atomic + CAS, memory_order choices, ABA mitigations and a small happens-before note.

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::atomic or locks like std::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, and relaxed define visibility and reordering; use acquire/release for most producer-consumer and seq_cst only when global ordering needed.

  • Atomic operations: std::atomic<T>::load/store/exchange and compare_exchange_weak/strong are the building blocks for lock-free algorithms; compare_exchange_weak may spuriously fail and is for loops.

  • Atomic fences: std::atomic_thread_fence enforces 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_order to 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: relaxed can avoid fences for counters where only per-thread accumulation and occasional aggregation occurs; use acquire/release for 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 relaxed memory order for correctness-sensitive synchronization — relaxed provides no ordering guarantees and will cause subtle visibility bugs when used instead of acquire/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_acquire load with memory_order_release store or use std::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

Related concepts