Facebook Pixel

The Producer Consumer Problem

The Producer Consumer Problem, also known as the Bounded Buffer Problem, is one of the most fundamental synchronization challenges in operating systems. It models any situation where one set of processes generates data and another set processes it, with a shared, fixed-size buffer sitting in between.

Getting this right means making sure producers never overflow the buffer and consumers never read from an empty one, all while multiple processes are running concurrently.

The Setup

The system has three parts:

  • Producers: Processes that generate data items and insert them into a shared buffer.
  • Consumers: Processes that remove data items from the buffer and process them.
  • Bounded Buffer: A fixed-size storage area (say N slots) shared between producers and consumers. It operates as a circular queue with an 'in' pointer for writing and an 'out' pointer for reading.
Producer Consumer Problem Diagram

Producer Consumer Problem: producers insert items into a bounded buffer, consumers remove them.

What Goes Wrong Without Synchronization

Without proper synchronization, five distinct problems can occur:

ProblemWhat HappensRoot Cause
Buffer OverflowA producer writes to a full buffer, overwriting data the consumer has not read yet.No mechanism to block the producer when the buffer is full.
Buffer UnderflowA consumer reads from an empty buffer, getting garbage or stale data.No mechanism to block the consumer when the buffer is empty.
Lost UpdatesTwo producers write to the same slot at the same moment. One write is lost.No mutual exclusion on the buffer during writes.
Duplicate ReadsTwo consumers read the same slot before either updates the out pointer.No mutual exclusion on the buffer during reads.
Inconsistent Pointer StateThe in or out pointer gets corrupted because two processes modify it simultaneously.Non-atomic read-modify-write on shared index variables.

The Three-Semaphore Solution

The standard solution uses three synchronization primitives, each handling a different concern:

SemaphoreInitial ValuePurpose
emptyN (buffer size)Counts available empty slots. Producers wait on this. Blocks when buffer is full.
full0Counts filled slots with data. Consumers wait on this. Blocks when buffer is empty.
mutex1Binary semaphore protecting the actual buffer read/write operations. Ensures only one process touches the buffer at a time.
text
--- Producer ---                      --- Consumer ---

wait(empty)    // Any empty slots?    wait(full)     // Any filled slots?
wait(mutex)    // Lock the buffer     wait(mutex)    // Lock the buffer

  buffer[in] = item                    item = buffer[out]
  in = (in + 1) % N                    out = (out + 1) % N

signal(mutex)  // Unlock the buffer   signal(mutex)  // Unlock the buffer
signal(full)   // One more item        signal(empty)  // One more slot
Critical: Semaphore Before Mutex
The order of wait() calls is not interchangeable. The counting semaphore (empty or full) must ALWAYS be acquired BEFORE the mutex. If you lock the mutex first and then block on empty/full, you are holding the mutex while sleeping. No other process can access the buffer, including the one that would unblock you. This is a guaranteed deadlock.

Step-by-Step Trace

Buffer size N = 3. One producer, one consumer.

text
Initial: empty=3, full=0, mutex=1, buffer=[ _, _, _ ]

Producer: wait(empty) -> empty=2, wait(mutex) -> mutex=0
          buffer[0] = A, in=1
          signal(mutex) -> mutex=1, signal(full) -> full=1
          Buffer: [ A, _, _ ]

Producer: wait(empty) -> empty=1, wait(mutex) -> mutex=0
          buffer[1] = B, in=2
          signal(mutex) -> mutex=1, signal(full) -> full=2
          Buffer: [ A, B, _ ]

Consumer: wait(full) -> full=1, wait(mutex) -> mutex=0
          item = buffer[0] = A, out=1
          signal(mutex) -> mutex=1, signal(empty) -> empty=2
          Buffer: [ _, B, _ ]

Producer: wait(empty) -> empty=1, wait(mutex) -> mutex=0
          buffer[2] = C, in=0  (circular wrap)
          signal(mutex) -> mutex=1, signal(full) -> full=2
          Buffer: [ _, B, C ]

Full Implementation in C++

cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <semaphore>
#include <vector>
#include <chrono>
#include <random>

const int BUFFER_SIZE = 5;
int buffer[BUFFER_SIZE];
int in = 0;   // Producer write index
int out = 0;  // Consumer read index

std::counting_semaphore<BUFFER_SIZE> empty_slots(BUFFER_SIZE);
std::counting_semaphore<BUFFER_SIZE> filled_slots(0);
std::mutex buffer_mutex;

std::mt19937 rng(std::random_device{}());
std::uniform_int_distribution<int> dist(1, 100);

void producer(int id) {
    for (int i = 0; i < 5; i++) {
        int item = dist(rng);

        empty_slots.acquire();           // Wait for empty slot
        buffer_mutex.lock();             // Lock the buffer

        buffer[in] = item;
        std::cout << "Producer " << id << " produced " << item
                  << " at slot " << in << std::endl;
        in = (in + 1) % BUFFER_SIZE;     // Circular increment

        buffer_mutex.unlock();           // Unlock the buffer
        filled_slots.release();          // Signal: item available

        std::this_thread::sleep_for(std::chrono::milliseconds(200));
    }
}

void consumer(int id) {
    for (int i = 0; i < 5; i++) {
        filled_slots.acquire();          // Wait for filled slot
        buffer_mutex.lock();             // Lock the buffer

        int item = buffer[out];
        std::cout << "Consumer " << id << " consumed " << item
                  << " from slot " << out << std::endl;
        out = (out + 1) % BUFFER_SIZE;   // Circular increment

        buffer_mutex.unlock();           // Unlock the buffer
        empty_slots.release();           // Signal: slot freed

        std::this_thread::sleep_for(std::chrono::milliseconds(300));
    }
}

int main() {
    std::cout << "Producer Consumer Simulation" << std::endl;
    std::cout << "Buffer Size: " << BUFFER_SIZE << std::endl;
    std::cout << "-----------------------------" << std::endl;

    std::thread p1(producer, 1), p2(producer, 2);
    std::thread c1(consumer, 1), c2(consumer, 2);

    p1.join(); p2.join();
    c1.join(); c2.join();

    std::cout << "-----------------------------" << std::endl;
    std::cout << "Simulation complete." << std::endl;
    return 0;
}

Output:

text
Producer Consumer Simulation
Buffer Size: 5
-----------------------------
Producer 1 produced 42 at slot 0
Producer 2 produced 87 at slot 1
Consumer 1 consumed 42 from slot 0
Producer 1 produced 23 at slot 2
Consumer 2 consumed 87 from slot 1
Producer 2 produced 65 at slot 3
Consumer 1 consumed 23 from slot 2
...
-----------------------------
Simulation complete.

Where This Shows Up in Real Systems

SystemProducerBufferConsumer
Web ServerNetwork thread accepting HTTP requestsRequest queueWorker threads processing requests
Print SpoolerApplications submitting print jobsPrint queuePrinter driver sending data to hardware
Video StreamingNetwork thread downloading video chunksFrame bufferVideo player rendering frames on screen
Logging PipelineApplication threads generating log entriesLog buffer (ring buffer)File writer flushing logs to disk
OS Pipe (cmd1 | cmd2)First command writing stdoutKernel pipe buffer (typically 64KB)Second command reading stdin

Common Mistakes

MistakeWhat Happens
Swapping wait(empty) and wait(mutex) in the producerDeadlock. Producer holds the mutex while blocked on empty. Consumer cannot acquire the mutex to free a slot.
Forgetting signal(full) after producingConsumer blocks forever on wait(full). It never knows items were added.
Forgetting signal(empty) after consumingProducer blocks forever on wait(empty). It never knows slots were freed.
Using a single semaphore for both empty and fullCannot distinguish between "buffer full" and "buffer empty". Both conditions collapse into one.

The Producer Consumer Problem is the foundation for understanding how data flows through concurrent systems. Every pipeline, queue, and buffered I/O channel is some variation of this pattern. Mastering the three-semaphore solution and understanding why each piece exists gives you the tools to reason about far more complex synchronization scenarios.

Stop and Think

A system has 2 producers and 1 consumer sharing a buffer of size 3. Both producers are fast (producing every 100ms) and the consumer is slow (consuming every 500ms). What happens over time, and which semaphore is responsible for handling it?
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.