Synchronization Primitives And Deadlock
Asked of: Software Engineer
Last updated
What's being tested
This topic tests whether you can reason precisely about concurrency, choose an appropriate synchronization primitive, and explain the correctness and performance consequences. An interviewer is probing beyond “put a mutex around it”: can you identify the shared invariant, define the critical section, account for memory visibility, and prevent races without serializing the entire system? For OpenAI-scale services, these skills matter because high-throughput request handling, schedulers, caches, queues, and resource managers all combine parallelism with failure-sensitive shared state.
Core knowledge
-
A data race occurs when two threads access the same memory concurrently, at least one access is a write, and there is no established happens-before relationship. A race can produce undefined behavior in C++, not merely a stale value.
-
A mutex provides mutual exclusion and typically ownership: one thread locks, performs a short critical section, then unlocks. Prefer scoped guards such as C++
`std::lock_guard`or`std::unique_lock`so exceptions and early returns cannot leak the lock. -
A semaphore represents permits rather than ownership. A counting semaphore is appropriate for limiting concurrent access to a pool of size ; a binary semaphore is not automatically equivalent to a mutex because ownership and misuse detection differ.
-
Use condition variables when a thread must wait for a state predicate, such as “queue is nonempty.” Always wait in a loop—
`cv.wait(lock, predicate)`—because wakeups can be spurious and another consumer may observe the state first. -
Atomics are ideal for independent counters, flags, and lock-free state transitions, but atomic individual fields do not make a multi-field invariant atomic. Choose memory ordering deliberately:
relaxedprovides atomicity, while acquire/release establishes visibility across threads. -
A deadlock requires four Coffman conditions: mutual exclusion, hold-and-wait, no preemption, and circular wait. Break any one—for example, impose a global lock order to eliminate circular wait—or use timeouts, cancellation, or preemption where appropriate.
-
Lock ordering assigns a total or partial order to locks; every code path acquires them in ascending order. If code may need locks
`A`and`B`, consistently acquire`A`before`B`, including error paths and callbacks. -
Avoid calling unknown code while holding a lock: logging, user callbacks, allocation, network I/O, and waiting on futures can re-enter the component or acquire locks in an undocumented order. Keep critical sections small, but do not split them so far that invariants become temporarily observable.
-
Reader-writer locks can improve read-heavy workloads but may starve writers, incur upgrade complexity, and perform worse than a mutex under contention. Measure contention and workload distribution rather than assuming parallel reads are beneficial.
-
A livelock occurs when threads remain active but make no progress, often due to repeated retries or symmetric backoff. Starvation occurs when one thread waits indefinitely while others progress; fairness guarantees, queueing, or bounded retries address it differently from deadlock prevention.
-
For producer-consumer structures, protect both the queue and its predicate under the same mutex: enqueue, update state, and notify while synchronized. A notification is not a stored event, so notifying before the waiter begins waiting is safe only when the predicate is maintained correctly.
-
Deadlock detection can use a wait-for graph: vertices are threads or transactions, and an edge means waits for a resource held by . A cycle indicates deadlock; production systems may instead use lock-order instrumentation, watchdogs, thread dumps, or database deadlock detectors.
Worked example
No concrete interview question was supplied, so consider the representative prompt: “Design a thread-safe bounded blocking queue.” In the first 30 seconds, clarify whether multiple producers and consumers are supported, whether `put` and `take` block or time out, what shutdown means, and whether FIFO fairness is required. I would organize the answer around the queue invariant and capacity, synchronization and memory visibility, blocking predicates, and shutdown/error behavior. A standard design uses one mutex, `not_empty` and `not_full` condition variables, and predicates `size > 0` and `size < capacity`. Producers acquire the mutex, wait in a loop for space, append, unlock, and notify consumers; consumers symmetrically wait for an item, remove one, unlock, and notify producers. I would explicitly choose `notify_one` for ordinary throughput, while noting that shutdown may require `notify_all` so every waiter can observe the terminal state. The key tradeoff is one mutex for simplicity and a clear invariant versus separate locks for higher concurrency and substantially greater correctness risk. I would close by saying that, with more time, I would discuss cancellation, bounded wait time, fairness, metrics for queue depth and wait duration, and stress testing under forced thread interleavings.
A second angle
The same reasoning applies to a thread-safe cache, but the dominant constraint changes from blocking coordination to read/write contention and eviction consistency. I would clarify whether stale reads are acceptable, whether values are expensive to compute, and whether eviction must be exact under concurrency. A mutex may be sufficient for a small cache, while sharding by hash can reduce contention; each shard then needs a clearly documented lock scope. If cache misses compute values while holding a lock, unrelated requests can stall or recursive computation can deadlock, so single-flight coordination or compute-outside-lock patterns deserve explicit discussion.
Common pitfalls
Pitfall: Saying “use an atomic counter” for a compound invariant such as
`size`plus queue contents. The counter may be atomic while the enqueue/dequeue relationship remains racy; identify the complete state that must change together.
Pitfall: Listing mutexes, semaphores, and condition variables without stating the predicate and ownership model. A stronger answer explains who owns each resource, what condition a waiter checks, and which operation changes that condition.
Pitfall: Claiming that lock ordering “prevents all concurrency bugs.” It prevents a class of circular-wait deadlocks, but not races, livelocks, starvation, forgotten unlocks, use-after-free, or deadlocks caused by hidden locks and callbacks.
Connections
An interviewer may pivot to lock-free algorithms, memory models, and linearizability, asking where an atomic compare-and-swap fits. Other natural connections include distributed locks, database transaction deadlocks, actor/message-passing designs, and observability through contention metrics, thread dumps, and scheduler traces.
Further reading
-
Operating Systems: Three Easy Pieces, chapters on Concurrency — clear treatment of locks, condition variables, semaphores, and common concurrency failures.
-
C++ Concurrency in Action, Anthony Williams — practical coverage of C++ memory ordering, locking patterns, and thread-safe design.
-
[Java Concurrency in Practice, Brian Goetz et al.] — foundational discussion of happens-before relationships, safe publication, and composing concurrent components.
Related concepts
- Concurrency, Deadlocks, And SynchronizationSoftware Engineering Fundamentals
- Concurrency And SynchronizationSoftware Engineering Fundamentals
- Thread-Safe Queues And Concurrency PrimitivesCoding & Algorithms
- Concurrency Control And Thread SafetySystem Design
- Concurrency, OS Threads, And MemorySoftware Engineering Fundamentals
- Concurrent Queues, Ring Buffers, And Lock-Free SynchronizationSoftware Engineering Fundamentals