Coding Interview Patterns Module 31 – Concurrency coding problems
Bounded buffers and ordered workers
The producer–consumer problem: a bounded buffer that makes fast producers wait, threads that take turns in a fixed order, and a clean shutdown when work ends.
What you will learn
- Implement a bounded producer–consumer buffer with a lock and two condition variables
- Explain backpressure with a timeline of a buffer that fills up
- Order threads with one semaphore or channel per hand-off
- Prevent consumers from waiting for good, with sentinels, closed channels or queue shutdown
- Reproduce the two classic wake-up bugs and fix them
Before you start
What you’ll learn · Before you start
What you will learn
- Implement a bounded producer–consumer buffer with a lock and two condition variables
- Explain backpressure with a timeline of a buffer that fills up
- Order threads with one semaphore or channel per hand-off
- Prevent consumers from waiting for good, with sentinels, closed channels or queue shutdown
- Reproduce the two classic wake-up bugs and fix them
Before you start
On this page
Bounded buffers sit between producers, which make work, and consumers, which do it. Producers put items in, consumers take them out, and the buffer never holds more than a fixed number: when it is full a producer waits, and when it is empty a consumer waits. That wait is the point. It is called backpressure: a fast producer cannot run ahead of slow consumers and fill memory, because it slows down to their pace. Ordered workers are the smallest case of the same idea. One thread may start its step only after another has finished its own, so each hand-off passes one signal, usually through a semaphore that starts at zero. Coding rounds ask for both bounded buffers and ordered workers, and then ask how the program stops cleanly once the work runs out.
A kitchen with a rail of three tickets
In a restaurant kitchen, two waiters take orders and clip each ticket on a rail that holds 3, and two chefs take tickets from the rail and cook. A waiter needs 1 second per order and a chef needs 4, so orders arrive four times as fast as the chefs can cook them.
A bounded buffer between producers and consumers, and ordered workers
Text description of the diagram
The figure has two parts.
At the top, a bounded buffer of 3. On the left are two producers, waiter A and waiter B, each with an arrow into the rail in the middle. The rail has three slots, all full, holding the tickets A2, B2 and A3. Under waiter B, its next ticket, B3, waits for room. On the right are two consumers, chef 1 and chef 2, each with an arrow from the rail. A note under the rail says that put() waits while the rail is full and get() waits while it is empty.
At the bottom, ordered workers. Three steps run on three threads, from left to right: connect, load and serve. An arrow labelled signal 1 goes from connect to load, and one labelled signal 2 from load to serve. Each thread waits for the signal before it, and gives the next one when its own step is done, so the steps run in that order however the threads are scheduled.
The program below runs that kitchen with asyncio.Queue(maxsize=3) as the rail: its put waits while the queue is
full and its get while it is empty. A fake clock simulates the seconds, so every run prints the same timeline.
kitchen/kitchen.py
"""Two waiters clip order tickets on a rail that holds 3, and two chefs take them and cook.
The rail is asyncio.Queue(maxsize=3): put() waits while it is full and get() while it is
empty. A waiter needs 1 simulated second to take an order and a chef 4 to cook one, so
orders arrive four times as fast as the two chefs can cook them."""
import asyncio
from fake_clock import FakeClock
ORDERS_EACH, TAKE_ORDER, COOK = 6, 1, 4
async def kitchen(rail_size, show_seconds=0):
clock = FakeClock()
rail = asyncio.Queue(maxsize=rail_size) # maxsize 0: no limit
on_rail, cooking, blocked = [], [], [] # what the timeline shows
clipped, cooked, waited, most = [], [], {"A": 0, "B": 0}, 0
async def waiter(name):
nonlocal most
for n in range(1, ORDERS_EACH + 1):
await clock.sleep(TAKE_ORDER)
ticket, asked = f"{name}{n}", clock.now
blocked.append(ticket)
await rail.put(ticket) # waits while the rail is full
blocked.remove(ticket)
on_rail.append(ticket)
waited[name] += clock.now - asked
clipped.append(ticket)
most = max(most, rail.qsize())
async def chef():
while True:
ticket = await rail.get() # waits while the rail is empty
on_rail.remove(ticket)
cooking.append(ticket)
await clock.sleep(COOK)
cooking.remove(ticket)
cooked.append(ticket)
rail.task_done()
async def timeline():
await clock.sleep(0.5) # look half-way between events
for _ in range(show_seconds):
print(f"{clock.now:5} s {' '.join(on_rail) or '-':12} {' '.join(cooking) or '-':8} "
f"{' '.join(blocked) or '-'}")
await clock.sleep(1)
async def service():
chefs = [asyncio.create_task(chef()) for _ in range(2)]
watcher = asyncio.create_task(timeline())
await asyncio.gather(waiter("A"), waiter("B"))
await rail.join() # every clipped ticket has been cooked
for task in [*chefs, watcher]:
task.cancel()
await clock.run(service())
return clock.now, sorted(clipped) == sorted(cooked) and len(cooked), waited, most
async def main():
print("time on the rail cooking waiting for room")
end, cooked, waited, most = await kitchen(3, show_seconds=8)
print(f"a rail of 3: {cooked} tickets cooked by {end} s; at most {most} on the rail; "
f"waiters waited {waited['A']} s and {waited['B']} s for room")
end, cooked, waited, most = await kitchen(0)
print(f"no limit: {cooked} tickets cooked by {end} s; at most {most} on the rail; "
f"waiters waited {waited['A']} s and {waited['B']} s for room")
asyncio.run(main()) Output
time on the rail cooking waiting for room 0.5 s - - - 1.5 s - A1 B1 - 2.5 s A2 B2 A1 B1 - 3.5 s A2 B2 A3 A1 B1 B3 4.5 s A2 B2 A3 A1 B1 B3 A4 5.5 s A3 B3 A4 A2 B2 - 6.5 s A3 B3 A4 A2 B2 B4 A5 7.5 s A3 B3 A4 A2 B2 B4 A5 a rail of 3: 12 tickets cooked by 25 s; at most 3 on the rail; waiters waited 7 s and 11 s for room no limit: 12 tickets cooked by 25 s; at most 8 on the rail; waiters waited 0 s and 0 s for room
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 kitchen.py
kitchen/fake_clock.py
"""A fake clock for asyncio: simulated seconds that pass only while every task waits.
Tasks call `await clock.sleep(seconds)` where real code would wait for a download.
`await clock.run(main())` runs a program: each time every task is waiting, it moves the
time on to the earliest wake-up and wakes that sleeper. Nothing really waits, so the
times a program prints are the same in every run and on every computer.
"""
import asyncio
import heapq
import itertools
# Turns of the event loop that let every task run on until it waits again. The programs
# of this lesson need at most 6; 12 leave room for a longer chain of awaits.
SETTLE_TURNS = 12
class FakeClock:
def __init__(self):
self.now = 0
self._sleepers = [] # (wake-up time, order, future), earliest first
self._order = itertools.count()
async def sleep(self, seconds):
wake = asyncio.get_running_loop().create_future()
heapq.heappush(self._sleepers, (self.now + seconds, next(self._order), wake))
await wake
async def run(self, main):
"""Runs the coroutine main to its end, and returns its result or raises its error."""
outcome = {}
async def capture():
try:
outcome["result"] = await main
except Exception as error: # handed to the caller below
outcome["error"] = error
task = asyncio.ensure_future(capture())
while True:
for _ in range(SETTLE_TURNS):
await asyncio.sleep(0)
if task.done():
if "error" in outcome:
raise outcome["error"]
return outcome["result"]
if not self._sleepers:
task.cancel()
raise RuntimeError(f"stuck at {self.now} s: every task waits, and none of them for the clock")
self.now, _, wake = heapq.heappop(self._sleepers)
wake.set_result(None) Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
From 3.5 seconds the rail is full, and a waiter with a new ticket waits for room until a chef takes one. Both kitchens finish the 12 tickets at 25 seconds, because the two chefs set the pace either way. What differs is the pile: with no limit, up to 8 tickets wait on the rail, and in a service that pile is memory that keeps growing for as long as the producers are faster. With a rail of 3, the waiters wait instead, 7 and 11 seconds in all.
Four tools build the rest of this lesson, with threads and with asyncio:
- One lock and two condition variables make a bounded buffer by hand: producers wait while it is full, consumers while it is empty.
- Blocking queues, such as
queue.Queue(maxsize), Java’sArrayBlockingQueueand Go’s buffered channels, do that waiting for you. - One semaphore per hand-off makes ordered workers: each step waits on a semaphore that the step before it releases.
- A sentinel per consumer, a Go channel closed by the sending side, or
Queue.shutdown()stops the consumers once the work runs out.
A bounded buffer from one lock and two conditions Pro: opens with a Pro, Premium or Ultimate pass
In this part:
- Two wake-up bugs, reproduced in a fixed order
- The kitchen with threads in Go and Java
- Go: a buffered channel, closed by the side that sends
- Java: ArrayBlockingQueue and a poison pill per chef
- Threads in a fixed order: one semaphore per hand-off
- Stopping cleanly: sentinels, closed channels and shutdown
- Sizing the buffer from a burst, with numbers
- Practise: a bounded queue that producers can close
- A bounded queue that producers can close
- Check yourself
- Interview questions
- What does put do on a full bounded queue, and why is that behaviour useful?
- A service hands log lines to a background thread through an unbounded queue. When the log server goes down for 10 minutes, memory grows until the service is killed. Redesign the hand-off, and say what happens to log lines while the log server is down.
- A pipeline reads files on 1 thread, parses them on 4 threads and writes rows to a database on 2 threads, with bounded queues between the stages. How do you stop it cleanly when the input ends, and what happens when one parser fails halfway?
- A start-up script launches three threads at once: one opens the database connection, one warms the cache, one starts serving requests. Warming needs the connection, and serving needs the warm cache. Make the order hold however the threads are scheduled, then extend it to n steps where some steps may run in parallel.
- Key takeaways
7 runnable examples (Kitchen.java, Python, kitchen.go, kitchen_session.py, session.py, start_order.go, wrong_close.go) · 1 exercise with sample tests · 3 interview answers · about 27 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
References
- threading: Condition objects and Semaphore objects (Python Software Foundation)
- queue: A synchronized queue class (Queue.shutdown) (Python Software Foundation)
- CPython source: Lib/queue.py (the Queue class) (Python Software Foundation)
- asyncio: Queues (Python Software Foundation)
- asyncio: Synchronization Primitives (Condition, Semaphore) (Python Software Foundation)
- The Go Programming Language Specification: Channel types (The Go Authors)
- The Go Programming Language Specification: Close (The Go Authors)
- Data Race Detector (The Go Authors)
- Java SE 25 API: ArrayBlockingQueue (Oracle)
- Java SE 25 API: ExecutorService (Oracle)
- Java SE 25 API: Semaphore (Oracle)
- Cooperating Sequential Processes (E. W. Dijkstra, EWD 123): 4.3 The Bounded Buffer (E. W. Dijkstra Archive, The University of Texas at Austin)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress