Articles

The Producer-Consumer Problem, Explained

The producer-consumer problem is a classic concurrency pattern: coordinating producers and consumers around a shared, bounded buffer safely.

The Lycoris Team The Lycoris Team · · 4 min read
Dark-themed code editor showing lines of code

The producer-consumer problem is a classic concurrency pattern where one or more producer processes generate data and place it into a shared buffer, while one or more consumer processes remove and process that data — and the two sides must coordinate safely without a shared memory location being read and written at the same time. It’s one of the oldest formal problems in concurrent programming, and it underlies an enormous share of real-world systems, from message queues to logging pipelines to thread pools.

The setup

Imagine a bounded buffer — a fixed-size queue — sitting between producers and consumers. A producer wants to add an item; a consumer wants to remove one. Two failure conditions define the problem:

  • Buffer overflow. A producer tries to add an item to a buffer that’s already full. It has to wait until a consumer makes room, rather than overwriting data or crashing.
  • Buffer underflow. A consumer tries to remove an item from a buffer that’s empty. It has to wait until a producer adds something, rather than reading garbage.

On top of that, if multiple producers or multiple consumers run concurrently, they must not both modify the buffer’s internal state (its head and tail pointers, its count) at the same time — that’s a race condition that can corrupt the buffer’s bookkeeping even if no individual operation looks wrong in isolation.

Why naive locking isn’t enough

The obvious first attempt is to wrap every buffer access in a single lock — a mutex that only one thread can hold at a time. That solves the race condition (no two threads mutate the buffer simultaneously) but doesn’t solve overflow or underflow. A producer that acquires the lock while the buffer is full still needs to know not to write and to release the lock so a consumer can drain it — a plain mutex has no concept of “wait until a condition becomes true.”

This is exactly the gap condition variables fill. A condition variable lets a thread holding a lock atomically release it and go to sleep, waiting to be woken when some condition changes, then reacquire the lock before continuing. A producer facing a full buffer waits on a “not full” condition; a consumer facing an empty buffer waits on a “not empty” condition. Whichever side changes the buffer’s state signals the appropriate condition, waking a waiting thread on the other side.

producer:
  lock(mutex)
  while buffer is full:
    wait(not_full, mutex)
  add item to buffer
  signal(not_empty)
  unlock(mutex)

consumer:
  lock(mutex)
  while buffer is empty:
    wait(not_empty, mutex)
  remove item from buffer
  signal(not_full)
  unlock(mutex)

The while loop around the wait — rather than a single if — matters: when a thread wakes from wait, the condition it was waiting for might have already been consumed by another thread that woke up first, so it has to recheck before proceeding. This is called a spurious-wakeup guard, and skipping it is one of the most common bugs in hand-rolled producer-consumer code.

The semaphore formulation

A second classic solution uses counting semaphores instead of condition variables — one semaphore tracking empty slots, initialized to the buffer’s capacity, and another tracking filled slots, initialized to zero, plus a mutex for mutual exclusion on the buffer itself:

producer:
  wait(empty_slots)
  lock(mutex)
  add item to buffer
  unlock(mutex)
  signal(filled_slots)

consumer:
  wait(filled_slots)
  lock(mutex)
  remove item from buffer
  unlock(mutex)
  signal(empty_slots)

The semaphore waits do double duty as both the coordination mechanism and the capacity limit — no separate while loop is needed because a semaphore’s count itself enforces the bound.

Where this pattern actually shows up

Almost every asynchronous system with a queue between two components is a producer-consumer problem in disguise. A message queue or broker like Kafka or RabbitMQ is a distributed, durable version of the same bounded-buffer coordination, scaled across machines instead of threads. A thread pool’s work queue is producers (whatever submits tasks) and consumers (worker threads) sharing a bounded queue. Backpressure — a consumer signaling it can’t keep up so producers slow down — is the producer-consumer problem’s answer to what happens when the buffer’s finite size isn’t just a technicality but a real constraint the whole system has to respect.

The takeaway

The producer-consumer problem formalizes a pattern that shows up constantly: two sides operating at different, unpredictable rates, coordinating through a shared bounded buffer without corrupting it or losing data. The mechanism — condition variables or semaphores plus a mutex — is less important than the underlying discipline: never let a producer write into a full buffer, never let a consumer read from an empty one, and never let concurrent access corrupt the buffer’s own bookkeeping. Modern queueing systems and thread pools are this same pattern, just distributed further and hidden behind a nicer API.

The Lycoris Team The Lycoris Team · · 4 min read

Mutex vs Semaphore: What's the Actual Difference

A mutex lets exactly one thread hold a lock at a time; a semaphore allows a fixed number of concurrent holders. Here's what that difference means in practice.

#Computer Science #Concurrency #Programming
The Lycoris Team The Lycoris Team · · 4 min read

What Is a Semaphore? Concurrency Control Explained

A semaphore is a counter that limits how many threads can access a resource at once. How they work, semaphores vs mutexes, and common pitfalls.

#Computer Science #Concurrency #Programming
The Lycoris Team The Lycoris Team · · 5 min read

The Dining Philosophers Problem, Explained

The dining philosophers problem is a classic model of deadlock and starvation: five philosophers, five forks, and a resource-sharing rule that can lock up.

#Computer Science #Concurrency #Programming