liveBook · Manning

Producer-consumer

As explained in Grokking Concurrency

The producer-consumer pattern coordinates tasks that generate data with tasks that process that data, typically through a shared buffer or queue.

Imagine a kitchen where cooks prepare dishes and servers carry them away. A shared counter lets both sides work at their own pace, but it must not overflow, and servers must not take from an empty counter. The producer–consumer pattern coordinates tasks that generate data with tasks that process it through a shared buffer. section 9.8

One or more producers add items while one or more consumers remove and process them. Synchronization prevents three failures: producers adding to a full buffer, consumers reading an empty buffer, and concurrent tasks conflicting while accessing the buffer. section 9.8

A standard solution assigns each synchronization tool a focused responsibility:

  • The empty semaphore counts available buffer slots. Producers acquire it before inserting and wait when the buffer is full. section 9.8
  • The full semaphore counts produced items. Consumers acquire it before removing and wait when the buffer is empty. section 9.8
  • A mutex protects the critical section so only one thread accesses the shared buffer at a time. section 9.8
# Chapter 9/producer_consumer.py
import time
from threading import Thread, Semaphore, Lock

SIZE = 5
BUFFER = ["" for i in range(SIZE)]                                   #1
producer_idx: int = 0
mutex = Lock()
empty = Semaphore(SIZE)
full = Semaphore(0)

class Producer(Thread):
    def __init__(self, name: str, maximum_items: int = 5):
        super().__init__()
        self.counter = 0
        self.name = name
        self.maximum_items = maximum_items

    def next_index(self, index: int) -> int:
        return (index + 1) % SIZE

    def run(self) -> None:
        global producer_idx
        while self.counter < self.maximum_items:
            empty.acquire()                                        #2
            mutex.acquire()                                        #3
            self.counter += 1
            BUFFER[producer_idx] = f"{self.name}-{self.counter}"
            print(f"{self.name} produced: "
                  f"'{BUFFER[producer_idx]}' into slot {producer_idx}")
            producer_idx = self.next_index(producer_idx)
            mutex.release()                                        #3
            full.release()                                         #4
            time.sleep(1)

class Consumer(Thread):
    def __init__(self, name: str, maximum_items: int = 10):
        super().__init__()
        self.name = name
        self.idx = 0
        self.counter = 0
        self.maximum_items = maximum_items

    def next_index(self) -> int:                                    #5
        return (self.idx + 1) % SIZE                                #5

    def run(self) -> None:
        while self.counter < self.maximum_items:
            full.acquire()                                          #6
            mutex.acquire()                                         #7
            item = BUFFER[self.idx]
            print(f"{self.name} consumed item: "
                  f"'{item}' from slot {self.idx}")
            self.idx = self.next_index()
            self.counter += 1
            mutex.release()                                         #8
            empty.release()                                         #9
            time.sleep(2)

if __name__ == "__main__":
    threads = [
        Producer("SpongeBob"),
        Producer("Patrick"),
        Consumer("Squidward")
    ]

    for thread in threads:
        thread.start()

    for thread in threads:
        thread.join()

Explore a Python implementation using threads, a fixed-size buffer, one mutex, and semaphores for empty slots and available items. section 9.8

How Grokking Concurrency covers it

How deep it goes
Explains semaphore counters, blocking conditions, critical sections, and mutex protection, but does not address implementation beyond the basic pattern.
Examples
Uses a fixed-size buffer with multiple threads, producer and consumer indices, a mutex, and full and empty semaphores.
What it assumes
Assumes basic concurrency and synchronization knowledge while reinforcing semaphores, mutexes, critical sections, and blocking.
What you can do after
Build it from scratch in Python using threads, a bounded buffer, semaphores, and a mutex.
How it connects
Links concurrency problems with race conditions, deadlocks, starvation, semaphores, mutexes, and the readers-writer pattern.

Also explained in

Compare how 3 books explain producer-consumer →

Related concepts

This explainer was generated by Manning's knowledge graph from the text of Grokking Concurrency. The section links point to the parts of the book it draws on.