Skip to content

← Concurrency with Threads and asyncio step 11 of 56

Medium Primitives

Build a Blocking Queue with a Condition

Build the buffer that sits between a producer thread and its consumers. The producer puts items in, and the consumers take them out, oldest first. The buffer holds at most capacity items, so the producer waits while it is full, and the consumers wait while it is empty. You are building a small queue.Queue on top of one threading.Condition.

buffer: BoundedBuffer[int] = BoundedBuffer(2)
buffer.put(1)
buffer.put(2)
buffer.put(3, timeout=0.1)  # waits 0.1 s for room, then raises TimeoutError
buffer.get()                # returns 1, the oldest item

What to write

Fill in BoundedBuffer[T]:

  • BoundedBuffer(capacity) raises ValueError if capacity is less than 1.
  • put(item, timeout=None) adds item at the back. If the buffer is full, it waits for room.
  • get(timeout=None) removes and returns the item at the front. If the buffer is empty, it waits for an item.
  • timeout is in seconds, and None means wait as long as it takes. If time runs out, raise TimeoutError and leave the buffer as it was.

Follow these rules:

  • Create one threading.Condition and store it as self._cond. Put every read and change of the buffer inside with self._cond:.
  • Wait on the condition. Don't call time.sleep, and don't use queue.Queue.
  • Call notify_all() after every successful put and get. Producers and consumers wait for different things, so all of them need a chance to check again.
  • Annotate the storage, for example self._items: list[T] = [], so the type checker knows that get on a BoundedBuffer[int] returns an int.

Edge cases

  • A timeout of zero or less doesn't wait. If put has no room right now, or get has no item, raise TimeoutError.
  • With items=0, only the -1 markers pass through, and the consumers must still finish.

What the driver checks

Keep solve as it is. It starts one producer and consumers consumer threads. The producer puts range(items) into the buffer, then one -1 for each consumer. A -1 tells a consumer to stop, and your buffer stores it like any other item. Afterwards, the driver fills the buffer and tries one more put. Then it empties the buffer and tries one more get. Both extra calls use a 0.05-second timeout. The report has these keys:

  • count and checksum are the number and the sum of the items the consumers received. They must equal items and sum(range(items)).
  • in_order is True when each consumer received its items in increasing order, as a first-in, first-out buffer hands them out.
  • put_timeout and get_timeout are True when those two extra calls raised TimeoutError.
  • sleeps counts calls to time.sleep. It must be 0.
  • uses_condition is True when self._cond is a threading.Condition.

The tests can pass with notify() in place of notify_all(), because the stall needs unlucky timing. Use notify_all() anyway.