Design a Multi-Producer Multi-Consumer Circular Buffer
NVIDIASoftware Engineer · Technical ScreenLast updated
Design a thread-safe, bounded multi-producer multi-consumer (MPMC) circular buffer supporting blocking operations, timeouts, and graceful shutdown (close-and-drain).
Design a Multi-Producer Multi-Consumer Circular Buffer
Implement a thread-safe, fixed-capacity circular buffer (bounded queue) that supports concurrent access by multiple producer threads and multiple consumer threads.
The queue must support blocking pushes and pops with optional timeouts, alongside a close() operation that prevents new items from being pushed while allowing existing items to be completely drained by consumers.
Interface
Implement the MPMCQueue class with the following methods:
class MPMCQueue:
def __init__(self, capacity: int):
"""Initialize the queue with a fixed positive capacity."""
pass
def push(self, item: object, timeout_ms: float | None = None) -> bool:
"""
Pushes an item into the queue. Blocks if the queue is full.
Returns True if the item was successfully added.
Returns False if timeout expires or the queue is closed before/during wait.
"""
pass
def pop(self, timeout_ms: float | None = None) -> object | None:
"""
Removes and returns an item from the queue. Blocks if empty.
Returns the item if available.
Returns None if timeout expires, or if the queue is closed and completely drained.
"""
pass
def close(self) -> None:
"""
Closes the queue. Prevents any new pushes.
Wakes all waiting producers (which fail) and waiting consumers (to drain or finish).
"""
pass
def size(self) -> int:
"""Returns the current number of elements in the buffer."""
pass
Requirements & Constraints
1 <= capacity <= 10^5- The internal storage must use a fixed-size array/list of length
capacitywith head and tail pointers wrapping around via modulo arithmetic. - Must be safe against race conditions, deadlocks, and spurious wakeups.
- Operations must provide proper publication safety: an item must be fully written to the slot before a consumer can read it.
close()is idempotent: multiple calls have no additional effect.
Example Walkthrough
q = MPMCQueue(capacity=2)
q.push("A") # Returns True, buffer = ["A"]
q.push("B") # Returns True, buffer = ["A", "B"]
# Thread 1 calls q.push("C", timeout_ms=50) -> blocks because buffer is full.
# Thread 2 calls q.pop() -> returns "A", wakes Thread 1.
# Thread 1 unblocks and places "C" -> returns True, buffer = ["B", "C"]
q.close()
q.push("D") # Returns False immediately (queue closed)
q.pop() # Returns "B"
q.pop() # Returns "C"
q.pop() # Returns None immediately (queue closed and empty)
Edge Cases
- Zero timeout: non-blocking attempt; fails immediately if full/empty.
- Multiple threads unblocked simultaneously when
close()is called. - High-contention churn where producers fill and consumers drain concurrently.
Hints
Hint 1
Use a single mutex combined with two condition variables (e.g., `not_full` and `not_empty`) to coordinate waiting threads.
Hint 2
Always wrap condition variable wait operations inside a `while` loop rather than an `if` statement to guard against spurious wakeups.
Hint 3
When `close()` is invoked, broadcast to both condition variables so all sleeping producers and consumers wake up to evaluate the closed state.
Follow-up questions
- What are the precise linearization points of the `push`, `pop`, and `close` operations in your implementation?
- How would you redesign this structure to be lock-free using atomic sequence numbers per slot (e.g., similar to the LMAX Disruptor or Dmitry Vyukov's bounded MPMC queue)?
- How can you mitigate cache-line bouncing (false sharing) between producer and consumer threads under heavy contention on multi-socket NUMA systems?
Solution walkthrough is for members
Get the full walkthrough with reference code and complexity analysis, plus Kitty Coach at 300 messages a day. Hints, the code runner and community answers stay free.