All chapters in this track
- 01Chapter 1: Concurrency vs. Parallelism, and Why It's Hard
- 02Chapter 2: Processes vs. Threads, Revisited for Concurrency
- 03Chapter 3: The Core Hazards: Race Conditions, Deadlock, Livelock, Starvation
- 04Chapter 4: Synchronization Primitives: Locks, Semaphores, Monitors, Condition Variables
- 05Chapter 5: Designing for Thread Safety: Immutability, Confinement, Thread-Safe Collections
- 06Chapter 6: Classic Patterns: Producer-Consumer, Reader-Writer, Thread Pools
- 07Chapter 7: Higher-Level Models: Async/Await, Event Loops, Coroutines, Actors, Channels
- 08Chapter 8: Memory Models and Visibility: Happens-Before, Volatile, Atomics
- 09Chapter 9: Lock-Free and Wait-Free Programming Basics
- 10Chapter 10: How Concurrency Differs Across Runtimes: JVM Threads, Python's GIL, Go's Goroutines, JavaScript's Event Loop
- 11Chapter 11: Debugging and Testing Concurrent Code
- 12Chapter 12: Classic Interview Problem: Producer-Consumer
- 13Chapter 13: Classic Interview Problem: Dining Philosophers
- 14Chapter 14: Classic Interview Problem: Readers-Writers
- 15Chapter 15: Concurrency in System Design and Production Debugging
Chapter 12 of 15
Chapter 12: Classic Interview Problem: Producer-Consumer
Why this matters
Producer-consumer (Chapter 6) is the single most commonly requested "implement this from scratch, using only basic primitives" concurrency interview problem — specifically because interviewers want to see whether you understand condition variables (Chapter 4) at a mechanical level, not whether you know queue.Queue exists. This chapter builds the from-scratch implementation, deliberately, with every design decision narrated.
The problem, precisely stated
"Implement a bounded buffer supporting put(item) and get(), safe for multiple producer and consumer threads, without using a pre-built thread-safe queue."
Building it from scratch, decision by decision
A naive first attempt — busy-waiting, which works but wastes CPU:
import threading
import time
class NaiveBoundedBuffer:
def __init__(self, capacity):
self._lock = threading.Lock()
self._items = []
self._capacity = capacity
def put(self, item):
while True:
with self._lock:
if len(self._items) < self._capacity:
self._items.append(item)
return
time.sleep(0.001) # BUSY-WAITING: repeatedly checking, wasting CPU while waiting
def get(self):
while True:
with self._lock:
if self._items:
return self._items.pop(0)
time.sleep(0.001) # same problem
What a strong candidate says here: "This is correct, but it busy-waits — every waiting thread repeatedly wakes up, acquires the lock, checks, and sleeps again, wasting CPU cycles that could be doing useful work elsewhere. I'd use a condition variable instead, so a waiting thread genuinely sleeps until it's specifically notified that something changed, rather than polling."
The correct implementation, using condition variables (Chapter 4):
import threading
class BoundedBuffer:
def __init__(self, capacity):
self._lock = threading.Lock()
self._not_full = threading.Condition(self._lock)
self._not_empty = threading.Condition(self._lock)
self._items = []
self._capacity = capacity
def put(self, item):
with self._not_full:
while len(self._items) >= self._capacity: # WHILE, not if -- guards against
self._not_full.wait() # spurious wakeups, per Chapter 4
self._items.append(item)
self._not_empty.notify() # wake ONE waiting consumer, since exactly one item became available
def get(self):
with self._not_empty:
while len(self._items) == 0:
self._not_empty.wait()
item = self._items.pop(0)
self._not_full.notify() # wake ONE waiting producer, since exactly one slot became available
return item
The design decisions a strong candidate narrates explicitly
[SENIOR INSIGHT]
-
Why two condition variables instead of one? Using a single condition variable would work correctly (since
notify_all()could wake every waiter, letting each re-check its own specific condition), but using two — one for "not full" (producers wait here) and one for "not empty" (consumers wait here) — meansnotify()only wakes a thread that could actually plausibly proceed, rather than waking every waiter (including ones that still can't proceed) and making them all re-check pointlessly. This is a genuine, deliberate efficiency choice worth explaining, not an arbitrary implementation detail. -
Why
notify()instead ofnotify_all()? Since exactly one slot/item became available, waking exactly one waiting thread is sufficient and avoids the "thundering herd" of waking every waiter just to have all but one immediately re-check and go back to waiting.notify_all()would still be correct (just less efficient) — worth mentioning this trade-off explicitly if asked. -
Why
whileinstead ofifaroundwait()? Directly Chapter 4's spurious-wakeup guard — a woken thread must re-verify its condition is actually true before proceeding, since a wakeup doesn't guarantee the specific condition it's waiting for has become true (another thread could have raced ahead and consumed the just-added item before this thread got scheduled).
Handling shutdown cleanly
[IN PRACTICE] A complete answer should also address how consumers know to stop — the "poison pill" pattern from Chapter 6 is the standard, simple answer, worth mentioning even if not explicitly asked:
SENTINEL = object() # a unique, unmistakable "stop" marker
def consumer(buffer):
while True:
item = buffer.get()
if item is SENTINEL:
break
process(item)
Common mistakes
- Using
ifinstead ofwhilearound thewait()call — the single most common, most important bug in a from-scratch implementation of this problem, and something interviewers specifically watch for. - Using one lock but forgetting condition variables entirely, falling back to the busy-waiting version without recognizing it as an inefficiency worth fixing — a strong candidate proactively identifies and improves on this, rather than needing to be prompted.
- Not explaining the reasoning behind design choices (two condition variables vs. one,
notify()vs.notify_all()) — as with the OOP book's Chapter 18 interview guidance, narrating the why behind each decision is a stronger signal than a working implementation delivered silently. - Forgetting to handle graceful shutdown — a fully "correct" bounded buffer that never terminates cleanly is an incomplete answer to how this pattern is actually used in real systems.
Discussion / interview questions
-
"Implement a thread-safe bounded buffer from scratch, without using a library queue." Strong answer: Produces the condition-variable-based implementation above, explicitly narrating why
while(notif) guards thewait()calls, and why the design uses two condition variables rather than one. -
"Why use two separate condition variables instead of one for this problem?" Strong answer: Using one condition variable would require
notify_all()to wake every waiter (since a single condition variable can't distinguish "producer waiting for space" from "consumer waiting for an item"), causing unnecessary wakeups and re-checks; two condition variables letnotify()target exactly the kind of waiter that can plausibly proceed. -
"What would happen if you used
ifinstead ofwhilearound the wait() calls in your implementation?" Strong answer: A spurious or stale wakeup could let a thread proceed without re-verifying its condition is actually true — e.g., a consumer could wake up believing an item is available, but another consumer might have already taken it first, leading to an incorrect state or an exception when trying to pop from an empty list. -
"How would you cleanly signal all consumer threads to stop processing?" Strong answer: A "poison pill" pattern — pushing a unique sentinel value (one per consumer thread) through the buffer, which each consumer recognizes as a stop signal and exits its processing loop upon receiving, rather than an external flag that could race with in-flight
get()calls.
Chapter summary / cheat sheet
- The producer-consumer interview problem tests condition variable mechanics specifically — a working busy-wait solution is a good starting point, but recognizing and fixing its CPU-wasting inefficiency with condition variables is the actual signal being evaluated.
- Two condition variables (not-full, not-empty) let
notify()target the specific kind of waiter that can plausibly proceed, rather than waking everyone. while(neverif) around everywait()call, guarding against spurious/stale wakeups — the single most-watched-for correctness detail.- A complete answer addresses graceful shutdown (the poison pill pattern), not just the core put/get mechanics.