Multithreading
Threads, synchronization, atomics, futures, and concurrency pitfalls.
43 questions
JuniorTheoryVery commonWhat is an atomic operation?
What is an atomic operation?
An atomic operation completes without visible intermediate state — other threads see it as fully done or not done. std::atomic<T> makes load, store, fetch_add, and compare_exchange indivisible.
Common mistakes
- ✗Assuming
i++on a non-atomic integer is atomic on x86 — the read-modify-write is three separate memory operations - ✗Using
std::atomicfor a struct and expecting the whole struct update to be atomic — only types withis_lock_freeare truly lock-free - ✗Thinking atomic operations have zero cost — LOCK instructions and cache coherence traffic can be expensive in tight loops
Follow-up questions
- →What is
compare_exchange_weakvscompare_exchange_strongand when do you use each? - →How does
std::atomic<T>::fetch_adddiffer fromx += nin a multithreaded context?
JuniorTheoryVery commonHow do you use std::mutex? RAII wrappers.
How do you use std::mutex? RAII wrappers.
Use RAII wrappers — never lock()/unlock() manually: lock_guard is simplest, unique_lock is movable and needed by condition_variable, scoped_lock (C++17) locks multiple mutexes atomically.
Common mistakes
- ✗Calling
mutex.lock()directly and then throwing — the lock is never released, deadlock guaranteed - ✗Using
lock_guardwhen the mutex must be unlocked before the scope ends — useunique_lockwith explicitunlock() - ✗Creating a mutex as a local variable in a function that is called from multiple threads — each call gets its own mutex, no synchronisation
Follow-up questions
- →What is
std::scoped_lockand how does it prevent deadlock when locking multiple mutexes? - →What happens if you move a
std::unique_lockwhile it holds the lock?
JuniorTheoryVery commonWhat is a race condition? How do you avoid it? What is a critical section?
What is a race condition? How do you avoid it? What is a critical section?
A data race is concurrent access to the same memory with at least one write and no sync — UB. A race condition is the broader logical bug where correctness depends on timing. A critical section must execute atomically.
Common mistakes
- ✗Fixing a data race by making a variable
volatile—volatiledoes not provide memory ordering - ✗Locking multiple mutexes in different orders in different threads — classic deadlock setup
- ✗Using
if (!flag) { flag = true; doWork(); }without atomics — flag check and set is not atomic
Follow-up questions
- →What is ThreadSanitizer and how does it detect data races at runtime?
- →How does
std::atomic<bool>eliminate the race in the flag example above?
JuniorTheoryVery commonHow do you synchronise data transfer between threads?
How do you synchronise data transfer between threads?
Mutex with lock_guard/unique_lock to protect shared data; condition_variable for producer-consumer; std::atomic for single-variable updates; std::promise/future for one-shot transfer.
Common mistakes
- ✗Locking a mutex and then sleeping inside the critical section — holds the lock while blocked, starving other threads
- ✗Using separate mutexes for related data — must always lock both in the same order or risk deadlock
- ✗Using
volatileinstead ofstd::atomicfor inter-thread flags —volatileprovides no memory ordering
Follow-up questions
- →What is the difference between
std::lock_guardandstd::unique_lock? - →When would you use
std::shared_mutexover a regularstd::mutex?
JuniorTheoryVery commonWhat happens if a std::thread is destroyed without join or detach?
What happens if a std::thread is destroyed without join or detach?
If a std::thread object is still joinable when its destructor runs, the program calls std::terminate() immediately. You must explicitly join or detach it before destruction.
Common mistakes
- ✗Letting exceptions skip a join call
- ✗Using detach while the thread still references stack variables
- ✗Confusing std::thread lifetime with OS thread completion
Follow-up questions
- →How does std::jthread improve this?
- →When is detach acceptable?
JuniorTheoryVery commonIs C++ thread-safe? What guarantees does the standard give?
Is C++ thread-safe? What guarantees does the standard give?
C++ guarantees concurrent reads of the same object, concurrent calls on different objects of the same type, std::atomic ops, and thread-safe init of function-local static (C++11). Concurrent read+write on a non-atomic object is a data race — UB.
Common mistakes
- ✗Thinking
constmeans thread-safe — aconstmethod can still callmutablemembers that have data races - ✗Accessing
std::coutfrom multiple threads — before C++20 this is only partially safe (characters may interleave) - ✗Assuming STL container operations are atomic — they are not; concurrent
push_backwithout a mutex is UB
Follow-up questions
- →How does the C++11 memory model define 'data race' formally?
- →What is the
constand thread-safety design guideline from Herb Sutter?
MiddleTheoryVery commonHow do you use std::condition_variable? What are spurious wakeups?
How do you use std::condition_variable? What are spurious wakeups?
Lock a unique_lock, then call cv.wait(lock, predicate) — it atomically releases the lock and blocks. On notify the thread re-acquires the lock and re-checks the predicate. A spurious wakeup is when wait returns without a notify.
Common mistakes
- ✗Using
cv.wait(lock)without a predicate — spurious wakeups cause the thread to proceed when the condition is not met - ✗Calling
notify_one/notify_allwhile not holding the mutex — can cause lost wakeups in rare race windows - ✗Using
condition_variablewithlock_guard—waitrequiresunique_lockbecause it must unlock/relock
Follow-up questions
- →What is the difference between
notify_oneandnotify_alland when do you use each? - →How do you use
condition_variable::wait_forto implement a timed wait with a timeout?
MiddleTheoryVery commonWhat is a deadlock and how can you prevent it?
What is a deadlock and how can you prevent it?
Deadlock is when threads wait on each other forever — usually from locking mutexes in different orders. Prevent it with a global lock order, short scopes, and std::scoped_lock for multiple mutexes.
Common mistakes
- ✗Locking A then B in one path and B then A in another
- ✗Calling user code while holding a mutex
- ✗Holding locks during blocking I/O
Follow-up questions
- →What does std::scoped_lock do with multiple mutexes?
- →What is livelock?
MiddleTheoryVery commonWhich synchronisation primitives does C++ provide? Advantages of lock_guard.
Which synchronisation primitives does C++ provide? Advantages of lock_guard.
Mutexes, locks (lock_guard/unique_lock/scoped_lock), condition_variable, atomics, and C++20 latch/barrier/semaphore. lock_guard is the simplest RAII wrapper: locks on construction, unlocks on destruction.
Common mistakes
- ✗Using
unique_lockeverywhere 'to be safe' — it has abool lockedflag and extra overhead;lock_guardis simpler and cheaper - ✗Not knowing
scoped_lockexists and manually implementing multi-mutex locking with potential deadlock - ✗Forgetting that
lock_guardcannot be moved — it is tied to the scope it was created in
Follow-up questions
- →When does
std::scoped_lock<M1, M2>guarantee deadlock-free locking of two mutexes? - →Can a
lock_guardbe used with atimed_mutex?
JuniorTheoryCommonWhat is the difference between concurrency and parallelism?
What is the difference between concurrency and parallelism?
Concurrency structures a program so multiple tasks make progress over overlapping time. Parallelism is executing tasks literally simultaneously on different cores. A concurrent program may run in parallel — but doesn't have to.
Common mistakes
- ✗Using the terms as synonyms
- ✗Assuming more threads always means faster execution
- ✗Ignoring synchronization overhead
Follow-up questions
- →How would you choose the number of worker threads?
- →What is a logical CPU core?
JuniorTheoryCommonWhat is the difference between a mutex and a semaphore?
What is the difference between a mutex and a semaphore?
A mutex has ownership — only the locker can unlock it. A semaphore is a signalling primitive with a counter; any thread can signal it. C++20 adds std::counting_semaphore/binary_semaphore.
Common mistakes
- ✗Using a mutex where a semaphore is needed for cross-thread signalling — a mutex can only be unlocked by its owner
- ✗Forgetting to call
V()on all code paths — the semaphore count stays low and other threads block forever (semaphore leak) - ✗Using a semaphore with count > 1 to protect a single resource — use a mutex instead
Follow-up questions
- →How do you implement a bounded producer-consumer queue using a counting semaphore?
- →What is a condition variable and how does it compare to a semaphore for wait/notify patterns?
MiddleTheoryCommonstd::launch::async vs std::launch::deferred. How does std::async work?
std::launch::async vs std::launch::deferred. How does std::async work?
launch::async runs the task on a new thread immediately; launch::deferred runs it lazily in the caller on get(). The default may silently defer, and the future destructor blocks under async.
Common mistakes
- ✗Not specifying launch policy and relying on the default — may run deferred when async was intended
- ✗Discarding the returned future — with
launch::async, the destructor blocks until the task finishes, creating an unintentional join - ✗Expecting
asyncto always create a new thread — implementations may use a thread pool with a limit
Follow-up questions
- →How do you cancel an async task launched with
std::async? - →What is the difference between
std::packaged_taskandstd::asyncin terms of control?
MiddleTheoryCommonWhat is the difference between multithreading and asynchrony?
What is the difference between multithreading and asynchrony?
Multithreading runs OS threads truly parallel on cores. Asynchrony starts a task and gets its result later, possibly on the same thread via an event loop. Threads give physical parallelism.
Common mistakes
- ✗Assuming
std::asyncalways runs on a new thread —launch::deferredruns on the calling thread - ✗Not calling
get()on astd::futurereturned bystd::async— the future destructor blocks until the task completes (forlaunch::async) - ✗Using coroutines and assuming the coroutine runs in parallel — a coroutine resumes on whatever thread calls
resume()
Follow-up questions
- →What is the difference between
std::asyncandstd::thread+std::promise? - →How do C++20 coroutines implement cooperative multitasking without threads?
MiddleTheoryCommonThread-safety guarantees of STL containers. Why is front() + pop_front() unsafe?
Thread-safety guarantees of STL containers. Why is front() + pop_front() unsafe?
Concurrent reads are safe; any concurrent write is a data race. front() + pop_front() is a TOCTOU race: thread A holds the reference while B calls pop_front() and destroys the element.
Common mistakes
- ✗Using a separate mutex for check and a different one for modify — TOCTOU window between releasing and re-acquiring
- ✗Calling
size()inside a conditional and then modifying based on the result without the lock held across both — classic check-then-act race - ✗Thinking
std::atomic<int>as a container size is enough — the container's internal state still has races
Follow-up questions
- →How would you implement a thread-safe queue that supports
try_dequeuewithout blocking? - →What is the ABA problem in lock-free queues and how do you prevent it?
MiddleTheoryCommonWhat does std::counting_semaphore provide that a std::mutex does not?
What does std::counting_semaphore provide that a std::mutex does not?
A std::counting_semaphore (C++20) holds a counter, so it admits up to N concurrent holders, not one. It has no ownership — any thread may release() a permit another thread acquire()d, which fits cross-thread signalling and bounded resource pools.
Common mistakes
- ✗Using a semaphore where a mutex fits — losing the ownership/lock-order checks that tools rely on
- ✗Forgetting to
release()on an error path, permanently shrinking the permit count (semaphore leak) - ✗Picking
counting_semaphore<1>for mutual exclusion instead of the clearerbinary_semaphore
Follow-up questions
- →How do you build a bounded buffer with two counting semaphores?
- →What is the role of the
LeastMaxValuetemplate parameter?
MiddleTheoryCommonWhat threading facilities does C++ provide? Common pitfalls.
What threading facilities does C++ provide? Common pitfalls.
C++11 added std::thread, mutexes, condition_variable, atomics, future/async. C++17 added shared_mutex and parallel STL. C++20 added jthread, latch, barrier, semaphore, coroutines.
Common mistakes
- ✗Destroying a
std::threadwithout joining or detaching — callsstd::terminate - ✗Calling
std::async(std::launch::async, ...)and discarding the returned future — the future destructor blocks - ✗Using
std::execution::paron algorithms that modify shared state — the parallelism makes it a data race
Follow-up questions
- →How does
std::jthreaddiffer fromstd::threadin terms of lifecycle management? - →What are
std::latchandstd::barrierand how do they differ?
MiddleDebuggingCommonWhy is this threaded counter wrong, and how to fix it?
Why is this threaded counter wrong, and how to fix it?
Data race: ++counter is an unsynchronized read-modify-write across threads, which is undefined behavior, so the final value is unpredictable. Fix with std::atomic<int> counter{0} (then ++counter is atomic) or guard the increment with a std::mutex + lock_guard.
Common mistakes
- ✗Believing
++is atomic because it is one operator or one instruction - ✗Thinking
volatileprovides thread-safety - ✗Assuming lost updates only slow it down rather than corrupting the result
Follow-up questions
- →Why is
volatilenot a substitute forstd::atomicin C++? - →When is a
std::mutexpreferable tostd::atomicfor a counter?
MiddleTheoryCommonWhat does C++20 std::jthread add over std::thread?
What does C++20 std::jthread add over std::thread?
std::jthread joins automatically in its destructor (no terminate() if you forget) and integrates with std::stop_token for cooperative cancellation via request_stop() and stop_requested().
Common mistakes
- ✗Mixing
jthreadwith manualjoin()in branches — fine, but redundant - ✗Forgetting that
request_stop()is cooperative — the thread function must check the token - ✗Using
std::threadand being surprised byterminate()when its destructor runs while joinable
Follow-up questions
- →How does
std::stop_callbackwork? - →Can you propagate stop requests to child threads?
MiddleTheoryCommonWhat do std::latch and std::barrier do, and how do they differ?
What do std::latch and std::barrier do, and how do they differ?
Both (C++20) make threads wait until a counter hits zero. A std::latch is single-use: count it down once and it is spent. A std::barrier is reusable: each arrive_and_wait resets it for the next phase and can run a completion function between phases.
Common mistakes
- ✗Trying to reuse a
std::latchfor a second phase — it cannot be reset, you need astd::barrier - ✗Calling
count_downmore times than the initial count, driving the counter negative (UB) - ✗Expecting the barrier completion function to run on a fixed thread — it runs on an unspecified arriving thread
Follow-up questions
- →When would you pick a
std::barrierover astd::condition_variable? - →What runs in the barrier's completion function and on which thread?
MiddleDebuggingCommonWhy can this lock ordering deadlock, and how to fix it?
Why can this lock ordering deadlock, and how to fix it?
Deadlock from lock-ordering inversion: t1 locks m1 then m2; t2 locks m2 then m1. If each takes its first lock at once, neither can take the second. Fix: lock in one consistent global order, or use std::scoped_lock lk(m1, m2) (C++17), which locks both atomically.
Common mistakes
- ✗Believing short critical sections cannot deadlock
- ✗Confusing this with double-locking a non-recursive mutex
- ✗Thinking lock_guard defers acquisition to scope exit
Follow-up questions
- →How does
std::scoped_lockavoid deadlock when locking multiple mutexes? - →What is a consistent lock-ordering discipline and why does it work?
MiddleTheoryCommonWhat is the C++11 memory model?
What is the C++11 memory model?
Defines when stores in one thread become visible to another via std::atomic with memory orderings (seq_cst, acq_rel, relaxed) and the happens-before relation. Before C++11 there was no portable threading model.
Common mistakes
- ✗Using
memory_order_relaxedfor a flag that signals readiness of other data — the flag is atomic, but the data writes may not be visible - ✗Thinking
seq_cstis free — it adds memory fences on ARM/Power architectures, measurable cost in tight loops - ✗Mixing C++ atomics with C11 atomics in the same program — technically UB due to different memory models
Follow-up questions
- →Explain acquire/release semantics with a producer-consumer example.
- →What is the difference between
memory_order_acq_reland usingmemory_order_acquireon load +memory_order_releaseon store?
MiddleTheoryCommonWhen would you use a mutex and when would you use an atomic?
When would you use a mutex and when would you use an atomic?
Use a mutex to protect invariants spanning multiple operations or variables. Use atomics for simple independent state like counters or flags. Atomics do not automatically make compound logic safe.
Common mistakes
- ✗Replacing a mutex with several atomics and breaking invariants
- ✗Using relaxed ordering without a correctness argument
- ✗Assuming atomic operations are always faster
Follow-up questions
- →What is memory_order_release/acquire?
- →Why can a mutex be faster under contention?
MiddleTheoryCommonWhat are the features of std::recursive_mutex?
What are the features of std::recursive_mutex?
std::recursive_mutex lets the same thread acquire the lock multiple times without deadlocking. lock() increments an internal counter and unlock() decrements it; release happens at zero.
Common mistakes
- ✗Using recursive_mutex by default because it is 'safer' — it is heavier than a plain mutex and hides design issues
- ✗Thinking recursive_mutex can be unlocked by another thread — it still has ownership; only the locking thread can unlock
- ✗Forgetting the lock count: locking N times requires exactly N unlocks; using RAII makes counting automatic
Follow-up questions
- →How would you refactor code that uses
recursive_mutexto avoid needing it? - →What is the cost difference between
std::mutexandstd::recursive_mutexon Linux?
MiddleTheoryCommonWhat is a read-write mutex (std::shared_mutex)?
What is a read-write mutex (std::shared_mutex)?
std::shared_mutex (C++17) has shared (read) mode for many threads and exclusive (write) mode for one. Use std::shared_lock for reads and std::unique_lock for writes.
Common mistakes
- ✗Using
shared_mutexfor write-heavy workloads — it adds overhead without benefit over a plain mutex - ✗Not benchmarking before switching from
std::mutextostd::shared_mutex— the extra state tracking can be slower for low reader counts - ✗Using the same
shared_mutexwith bothunique_lockandshared_lockin the wrong order — classic deadlock
Follow-up questions
- →When would you use
std::shared_timed_mutexinstead ofstd::shared_mutex? - →How would you implement a read-write lock using just
std::mutexandstd::condition_variable?
MiddleTheoryCommonHow does std::scoped_lock lock multiple mutexes without deadlock?
How does std::scoped_lock lock multiple mutexes without deadlock?
For several mutexes, std::scoped_lock (C++17) calls std::lock, a deadlock-avoidance algorithm: it tries, backs off and retries on conflict, so no fixed acquisition order is needed. It then holds them RAII-style and unlocks all in the destructor.
Common mistakes
- ✗Thinking the protection scales to mutexes locked by separate
scoped_lockobjects — only one combined call is deadlock-free - ✗Believing
scoped_lockwith a single mutex still runs the back-off algorithm — it just acts likelock_guard - ✗Locking a mutex manually and then passing it to
scoped_lock, causing a double-lock
Follow-up questions
- →How does
std::scoped_lockwith exactly one mutex differ fromstd::lock_guard? - →Can you pass already-locked mutexes via
std::adopt_lock?
MiddleTheoryCommonHow does a spinlock work? When is it better than a mutex?
How does a spinlock work? When is it better than a mutex?
A spinlock busy-waits in a loop instead of yielding the CPU, avoiding the syscall overhead of mutex sleep/wake. It is faster only when the critical section is very short; longer holds waste CPU.
Common mistakes
- ✗Using a spinlock for a critical section that may take more than a few hundred nanoseconds — burns CPU time
- ✗Implementing a spinlock without a
pause/yieldinstruction — on x86_mm_pause()reduces power and improves HT performance - ✗Using a spinlock in single-core environments — if the lock holder is preempted, the waiter spins forever
Follow-up questions
- →How does
std::atomic_flagimplement a minimal spinlock? - →What is the difference between a spinlock and a futex (fast userspace mutex)?
MiddleTheoryCommonWhat happens if an exception escapes a thread? Safe async tools.
What happens if an exception escapes a thread? Safe async tools.
An exception escaping a std::thread function calls std::terminate. To propagate across threads use std::future/std::async (rethrown on get()) or std::exception_ptr to capture and transport manually.
Common mistakes
- ✗Catching exceptions in a thread with
catch(...)and swallowing them — silent failure, hard to debug - ✗Not calling
future::get()— the exception stored in the future is silently discarded when the future destructs - ✗Using
std::threadfor tasks that can throw, without manual exception propagation — crash instead of propagation
Follow-up questions
- →How do you propagate multiple exceptions from multiple worker threads back to the main thread?
- →What is
std::exception_ptrand is it safe to copy across thread boundaries?
MiddleCodeCommonWrite a thread-safe thread pool.
Write a thread-safe thread pool.
Pre-spawn N workers waiting on a shared task queue protected by a mutex and condition variable. submit() enqueues a std::function<void()> and notifies one worker; the destructor sets a stop flag and joins.
Common mistakes
- ✗Not re-checking the predicate after
waitwakes up — spurious wakeups deliver the thread with an empty queue; always usewait(lock, pred)or a loop - ✗Destroying the pool while tasks are still queued — workers should drain the queue before exiting, or the destructor should decide whether to cancel or complete outstanding work
- ✗Blocking
submit()on a full queue without a size limit — unbounded queues can exhaust memory under load
Follow-up questions
- →How would you add priority support to the task queue?
- →How can you return a
std::futurefromsubmit()so callers can wait for results?
SeniorCodeCommonImplement producer-consumer using condition variables.
Implement producer-consumer using condition variables.
A bounded queue under a mutex with two condition variables: not_full (producer waits if full) and not_empty (consumer waits if empty). Graceful shutdown uses a done_ flag — consumers drain on done_ && queue_.empty().
Common mistakes
- ✗Using a single condition variable for both 'not full' and 'not empty' — requires
notify_all()and wastes CPU; two CVs withnotify_one()are more efficient - ✗Calling
notify_one()while holding the lock — technically correct but releases the woken thread only to block immediately on the mutex; consider notifying after releasing the lock - ✗Not handling the shutdown race — if
done_is set before consumers check it they may miss the last items; the predicate must bedone_ && queue_.empty()not justdone_
Follow-up questions
- →How would you implement a lock-free bounded queue for producer-consumer?
- →What is back-pressure and how do you implement it when the consumer is slower than the producer?
SeniorTheoryCommonWhat does thread_local do?
What does thread_local do?
thread_local gives each thread its own independent copy of a variable, initialized on first access and destroyed when the thread exits.
Common mistakes
- ✗Using
thread_localfor state that should be task-local in a thread pool — the thread reuses the state across tasks, which causes subtle bugs - ✗Forgetting that
thread_localclass static members must still be defined in exactly one translation unit (like any static member) - ✗Assuming
thread_localprovides synchronization — it eliminates sharing, not races from the same thread accessing the value from multiple coroutines or signal handlers
Follow-up questions
- →How does
thread_localinteract with dynamic libraries — is the variable shared across DSO boundaries? - →What is the cost of
thread_localaccess on Linux (TLS via%fsregister offset lookup)?
MiddleTheoryOccasionalWhat is std::atomic? Memory ordering options.
What is std::atomic? Memory ordering options.
std::atomic<T> makes read-modify-write operations indivisible. Orderings: relaxed, acquire/release, acq_rel, and seq_cst (total global order, default).
Common mistakes
- ✗Using
relaxedfor a done/ready flag that signals other data is ready — withoutrelease/acquire, the data writes may be invisible - ✗Assuming
fetch_addwithrelaxedis fine for a shared counter — it's fine for the count itself, but not if you need to synchronise on the result - ✗Using
atomic<std::string>— only trivially copyable types are lock-free;atomic<string>internally uses a mutex
Follow-up questions
- →What is a sequentially consistent total order and why is it the most intuitive but most expensive ordering?
- →How do you implement a lock-free stack using
compare_exchange_weak?
MiddleCodeOccasionalWrite a basic std::atomic<T> implementation.
Write a basic std::atomic<T> implementation.
A minimal Atomic<T> wraps a value and mutex with load/store/compare_exchange_strong. A real std::atomic<T> for trivially-copyable types uses lock-free CPU instructions via intrinsics — no mutex needed.
Common mistakes
- ✗Forgetting to protect
compare_exchange_strongatomically — a load + conditional store with a gap between them is not atomic and defeats the purpose - ✗Using
volatileinstead of atomics for inter-thread communication —volatileprevents reordering by the compiler but not by the CPU - ✗Omitting memory_order parameters — the default
memory_order_seq_cstis correct but the most expensive; for simple flagsmemory_order_acquire/releaseis sufficient
Follow-up questions
- →How does the CPU guarantee atomicity of a 64-bit read on x86-64 without a LOCK prefix?
- →What is ABA problem and how does
std::atomic<std::shared_ptr<T>>help?
MiddleTheoryOccasionalCan flock() synchronize threads within a single process?
Can flock() synchronize threads within a single process?
No. A flock() lock belongs to the open file description, not to a thread. All threads of one process share that fd, so they all see the same lock and flock() never blocks one against another. It is advisory locking between processes; use a std::mutex for threads.
Common mistakes
- ✗Treating
flock()as a general-purpose lock and reaching for it to guard in-process shared state - ✗Believing
flock()is mandatory — it is advisory: only cooperating callers that also callflock()are blocked - ✗Thinking the lock is owned per-thread or per-fd value rather than per open file description
Follow-up questions
- →Does
fcntl()(POSIX record) locking behave differently acrossfork()and threads? - →How does an
flock()lock interact with a descriptor duplicated bydup()?
MiddleTheoryOccasionalWhat is a livelock and how does it differ from a deadlock?
What is a livelock and how does it differ from a deadlock?
Deadlock: threads stuck waiting on each other with no progress. Livelock: threads run and change state in response to each other but no useful work happens — usually from naive retry logic without jitter.
Common mistakes
- ✗Spinning with
while(!try_lock()) yield()from many threads — degrades to livelock under contention - ✗Diagnosing high CPU usage as 'just busy' when it's livelock
- ✗Adding plain backoff without randomisation — N threads still synchronise on retries
Follow-up questions
- →What is exponential backoff with jitter?
- →How does the dining philosophers problem expose both deadlock and livelock?
MiddleTheoryOccasionalHow many threads should you use for a task? What does it depend on?
How many threads should you use for a task? What does it depend on?
CPU-bound tasks: one thread per logical core (hardware_concurrency()); I/O-bound tasks: more threads than cores since they mostly wait; Amdahl's Law caps the speedup by the sequential fraction.
Common mistakes
- ✗Creating one thread per request in a server — can exhaust OS limits; use a thread pool
- ✗Setting thread count to
hardware_concurrency()for disk I/O tasks — too few; disk threads spend most time waiting - ✗Not considering hyperthreading —
hardware_concurrency()returns logical cores (HT included); for compute tasks physical cores may be more meaningful
Follow-up questions
- →What is Amdahl's Law and how does it cap the maximum speedup for a parallel program?
- →How do work-stealing thread pools differ from fixed-assignment pools?
SeniorTheoryOccasionalWhat is the ABA problem in lock-free code, and how is it mitigated?
What is the ABA problem in lock-free code, and how is it mitigated?
ABA: a compare_exchange reads A, another thread changes it to B and back to A, so the CAS succeeds though the structure changed underneath. Mitigate it with a tagged pointer (a version counter bumped per update) or hazard pointers.
Common mistakes
- ✗Assuming a successful CAS means nothing changed — it only means the value matches, not the history
- ✗Hitting ABA hardest with freed-and-reallocated nodes that the allocator hands back at the same address
- ✗Adding a version counter but letting it wrap, so ABA reappears on a wide enough time window
Follow-up questions
- →How do hazard pointers solve the related safe-reclamation problem?
- →Why does a double-width CAS (
DCAS) help, and what hardware supports it?
SeniorTheoryOccasionalWhat is the double-checked locking pattern and why was it broken before C++11?
What is the double-checked locking pattern and why was it broken before C++11?
Double-checked locking reads a lazy pointer without lock, then locks and re-checks. Pre-C++11 it was UB because a thread could see a non-null pointer but uninitialised fields.
Common mistakes
- ✗Implementing double-checked locking with a non-atomic pointer in C++11+ — still UB
- ✗Forgetting that
volatiledoes not provide synchronization in C++ - ✗Not using
std::call_oncebecause the syntax feels heavier — it's the simplest correct option
Follow-up questions
- →Why does function-local static work for lazy init?
- →What's the cost of
std::call_onceafter the first call?
SeniorPerformanceOccasionalWhat is false sharing, and how do you eliminate it?
What is false sharing, and how do you eliminate it?
False sharing is when two threads write distinct variables that share one cache line — each write invalidates the other core's copy, causing line ping-pong. Fix it by aligning hot data to std::hardware_destructive_interference_size.
Common mistakes
- ✗Confusing false sharing with a real data race — it is a correctness-neutral performance bug, not a logic error
- ✗Packing per-thread counters into a tight array or struct, so adjacent elements share a cache line
- ✗Hardcoding 64 as the line size instead of
hardware_destructive_interference_size
Follow-up questions
- →How would you detect false sharing with
perfor VTune? - →What is true sharing, and why can padding not fix it?
SeniorTheoryOccasionalWhat is std::atomic_thread_fence and when do you need it instead of atomic operations?
What is std::atomic_thread_fence and when do you need it instead of atomic operations?
atomic_thread_fence(order) is a standalone barrier that orders prior and subsequent operations in the current thread without performing any atomic load or store. Putting release/acquire on the atomic op is usually simpler.
Common mistakes
- ✗Using
atomic_signal_fenceinstead ofatomic_thread_fence—signal_fenceonly orders within a single thread (signal handler), not across threads - ✗Believing fences alone synchronize — they need an atomic operation on the other thread to pair
- ✗Sprinkling fences instead of fixing the actual data race
Follow-up questions
- →Compare
atomic_thread_fence(release)withstore(release)on an atomic — when are they equivalent? - →Why does
atomic_signal_fencecost less thanatomic_thread_fence?
SeniorTheoryOccasionalDescribe lock-free data structures. Lock-free vs wait-free.
Describe lock-free data structures. Lock-free vs wait-free.
Lock-free: at least one thread always makes progress (no deadlock, but individuals can starve). Wait-free: every thread finishes in a bounded number of steps — the strongest guarantee.
Common mistakes
- ✗Thinking lock-free means faster than mutex — without contention a mutex is often faster; lock-free is for high-contention or real-time constraints
- ✗Not handling the ABA problem — leads to corrupt data structures that are hard to reproduce
- ✗Using
compare_exchange_weakwithout a retry loop — it can fail spuriously; always loop
Follow-up questions
- →How do hazard pointers solve the safe memory reclamation problem in lock-free structures?
- →What is the ABA problem and how does
std::atomic<std::shared_ptr<T>>(C++20) help?
SeniorTheoryOccasionalWhat are memory barriers and acquire/release semantics?
What are memory barriers and acquire/release semantics?
A memory barrier prevents reordering of memory ops across it. Acquire (load): later ops stay after; release (store): earlier ops stay before. A release/acquire pair makes the writer's prior writes visible to the reader.
Common mistakes
- ✗Using relaxed loads and stores for a flag that guards other data — the surrounding non-atomic writes may be reordered past the flag
- ✗Thinking barriers are needed only on ARM — while x86 is strongly ordered, compiler reordering still requires C++ atomics
- ✗Applying a full memory barrier (
seq_cst) where only acquire/release is needed — correct but wasteful on ARM/Power
Follow-up questions
- →What is the difference between a compiler barrier (
std::atomic_signal_fence) and a hardware memory barrier? - →Why does x86 not need explicit load/store barriers but ARM does?
SeniorTheoryOccasionalWhat is priority inversion and how is it commonly mitigated?
What is priority inversion and how is it commonly mitigated?
A high-priority thread waits on a lock held by a low-priority one while medium-priority threads preempt the holder. Mitigated by priority inheritance, priority ceiling, or short critical sections.
Common mistakes
- ✗Setting per-thread priorities without choosing an inversion-aware mutex
- ✗Long critical sections under contention — even without priorities, latency suffers
- ✗Mixing real-time priorities with non-real-time threads sharing locks
Follow-up questions
- →How does
pthread_mutexattr_setprotocol(PTHREAD_PRIO_INHERIT)work? - →Why are spinlocks dangerous in priority-inversion scenarios?
SeniorDesignOccasionalYou are designing a C++ class whose public API will be called from many threads at once, and it invokes user-supplied callbacks. The design must make the thread-safety guarantee of each method unambiguous to callers, prevent races such as a caller observing or acting on stale state between two calls, and avoid deadlock when a callback re-enters the object. Describe the principles you follow when shaping such an API.
You are designing a C++ class whose public API will be called from many threads at once, and it invokes user-supplied callbacks. The design must make the thread-safety guarantee of each method unambiguous to callers, prevent races such as a caller observing or acting on stale state between two calls, and avoid deadlock when a callback re-enters the object. Describe the principles you follow when shaping such an API.
Minimize shared mutable state, document thread-safety guarantees explicitly, encapsulate RAII locks at class boundaries, never hold locks across user callbacks, and expose atomic compound operations instead of separate check/act methods.
Common mistakes
- ✗Two-step operations exposed as two separate API calls — the gap between them creates a TOCTOU race; expose atomic compound operations instead
- ✗Returning references or iterators into internal data while holding no lock — the caller can't use them safely without knowing the lock protocol
- ✗Recursive locking with
std::mutexinstead ofstd::recursive_mutexwhen callbacks can re-enter the same class — leads to deadlock
Follow-up questions
- →How does the actor model (e.g., Akka) enforce thread-safety at the architecture level?
- →What is the 'monitor pattern' and how does it differ from ad-hoc locking?