Interview concept

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 NN; 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: relaxed provides 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 Ti→TjT_i \rightarrow T_j means TiT_i waits for a resource held by TjT_j. 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

Related concepts