Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Intermediate
  • 35 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, Go 1.27.1 and Java 25
  • By MySmartCoPilot
Free intro, Pro: 8 of 35 minutes free
Free: A kitchen with a rail of three tickets. With a pass: the other 7 sections and 3 interview answers, about 27 minutes.

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.

Two waiters feed a full rail of 3 tickets that two chefs take from; below, connect, load and serve pass one signal each to the next.A bounded buffer of 3waiter Awaiter BB3 waitsA2B2A3the rail: fullchef 1chef 2put() waits while full,get() waits while emptyOrdered workers: one signal per hand-offconnectloadservesignal 1signal 2Each thread waits for the signal before it,and gives the next one when its step is done

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.

The kitchen, with a rail of 3 and with no limit

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)

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’s ArrayBlockingQueue and 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.

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

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.