Implementing the Producer-Consumer Algorithm in C++

In this post, we implement a simulation of the Producer-Consumer Problem in C++ using a circular bounded buffer. This is one of the most classic synchronization problems in operating systems, modeling the coordination between a process that generates data (producer) and a process that consumes data (consumer) through a shared buffer of fixed capacity.

What is the Producer-Consumer Problem?

The Producer-Consumer problem (also called the Bounded Buffer problem) defines these rules:

  • The producer generates items and places them into the buffer β€” but only if space is available. If the buffer is full, the producer must wait (sleep).
  • The consumer retrieves and processes items from the buffer β€” but only if items exist. If the buffer is empty, the consumer must wait (sleep).
  • Only one entity should access the buffer at a time (mutual exclusion).

This simulation runs 20 steps. At each step, a random number determines whether the producer or consumer acts. A circular buffer is used so the array can be reused efficiently without shifting elements.


C++ Code Implementation

// ============================================================
// Producer-Consumer Problem in C++ (Single-threaded Simulation)
// Uses a circular bounded buffer; random decisions simulate
// concurrent producer/consumer behavior without real threads
// ============================================================

#include <iostream>
#include <cstdlib>   // for rand()
using namespace std;

int main() {

    int buffer[50];    // The shared circular buffer
    int bufferSize;    // Maximum capacity of the buffer

    cout << "\n Enter Buffer Size: ";
    cin  >> bufferSize;

    int itemCount    = 0;   // Current number of items in the buffer
    int producePtr   = 0;   // Points to the next write position (producer's index)
    int consumePtr   = 0;   // Points to the next read position (consumer's index)
    int stepCounter  = 1;   // Tracks simulation step number (items are numbered by step)

    // -------------------------------------------------------
    // Simulation: run for 20 steps
    // -------------------------------------------------------
    while (stepCounter <= 20) {

        // --- Display current buffer contents ---
        // Walk the circular buffer from consumePtr to producePtr
        cout << "\n\n Buffer: ";
        int displayIdx = consumePtr % bufferSize;
        while (displayIdx != producePtr % bufferSize) {
            cout << buffer[displayIdx] << "\t";
            displayIdx++;
            if (displayIdx == bufferSize) displayIdx = 0;   // Wrap around
        }

        // --- Random decision: even = producer acts, odd = consumer acts ---
        int randomDecision = rand() % 100;
        stepCounter++;

        if (randomDecision % 2 == 0) {
            // ---- Producer's turn ----
            if (itemCount != bufferSize) {
                // Buffer not full: produce a new item
                buffer[producePtr % bufferSize] = stepCounter;  // Place item at produce pointer
                cout << "\n Producer Produced: " << stepCounter;
                producePtr++;  // Advance the produce pointer
                itemCount++;   // One more item in the buffer
            } else {
                // Buffer full: producer must wait
                cout << "\n Producer is Sleeping (Buffer Full)";
            }
        } else {
            // ---- Consumer's turn ----
            if (itemCount != 0) {
                // Buffer not empty: consume the oldest item
                int consumedItem = buffer[consumePtr % bufferSize];  // Read from consume pointer
                cout << "\n Consumer Consumed: " << consumedItem;
                consumePtr++;  // Advance the consume pointer
                itemCount--;   // One fewer item in the buffer
            } else {
                // Buffer empty: consumer must wait
                cout << "\n Consumer is Sleeping (Buffer Empty)";
            }
        }
    }

    cout << endl;
    return 0;
}

Explanation of the Code

  1. Circular Buffer β€” The buffer is accessed using producePtr % bufferSize and consumePtr % bufferSize. As the pointers grow, the modulo operation wraps them back to the start of the array, allowing the fixed-size buffer to be reused indefinitely without shifting data.
  2. itemCount β€” Tracks how many items are currently in the buffer. itemCount == bufferSize means full (producer sleeps). itemCount == 0 means empty (consumer sleeps). This is the semaphore analog in a real synchronization solution.
  3. Random decision β€” rand() % 100 produces a value 0–99. Even numbers trigger the producer; odd numbers trigger the consumer. This simulates unpredictable concurrent scheduling without actual threads.
  4. Buffer display β€” Walks from consumePtr % bufferSize to producePtr % bufferSize, printing all items currently in the buffer (oldest to newest). The if (displayIdx == bufferSize) displayIdx = 0 handles wrap-around during display.
  5. Variable names β€” producePtr / consumePtr clearly indicate their role. itemCount is self-explanatory. randomDecision makes the intent of the random number obvious.

Sample Output

 Enter Buffer Size: 6

 Buffer:
 Consumer is Sleeping (Buffer Empty)

 Buffer:
 Consumer is Sleeping (Buffer Empty)

 Buffer:
 Producer Produced: 4

 Buffer: 4
 Producer Produced: 5

 Buffer: 4    5
 Consumer Consumed: 4

 Buffer: 5
 Producer Produced: 7

 Buffer: 5    7
 Producer Produced: 8

 Buffer: 5    7    8
 Producer Produced: 9

 Buffer: 5    7    8    9
 Producer Produced: 10

 Buffer: 5    7    8    9    10
 Producer Produced: 11

 Buffer: 5    7    8    9    10    11
 Consumer Consumed: 5

 Buffer: 7    8    9    10    11
 Consumer Consumed: 7

 Buffer: 8    9    10    11
 Consumer Consumed: 8

 Buffer: 9    10    11
 Consumer Consumed: 9

 Buffer: 10    11
 Consumer Consumed: 10

 Buffer: 11
 Consumer Consumed: 11

 Buffer:
 Consumer is Sleeping (Buffer Empty)

 Buffer:
 Producer Produced: 19

 Buffer: 19
 Consumer Consumed: 19

Step-by-Step Explanation of Input/Output

  • Steps 1–2: Random selects consumer both times. Buffer is empty β†’ Consumer sleeps both times.
  • Steps 3–4: Producer gets selected. Items 4 and 5 are produced and placed in the buffer.
  • Step 5: Consumer consumes item 4 (oldest = front of circular buffer).
  • Steps 6–11: Producer keeps getting selected. Items 7–11 fill the buffer to capacity (6 items).
  • Steps 12–17: Consumer repeatedly selected. Items 5, 7, 8, 9, 10, 11 consumed one by one until buffer empties.
  • Step 18: Consumer selected but buffer empty β†’ sleeps.
  • Steps 19–20: Producer produces item 19; consumer immediately consumes it.

Note: The output will differ on each run because rand() produces a different sequence unless explicitly seeded with srand().


See Also

Bottom line is…

This simulation captures the core logic of the bounded buffer problem in a single-threaded program. In a real concurrent system, the itemCount variable would be replaced with semaphores (sem_wait / sem_post in POSIX or BlockingQueue in Java) to ensure thread-safe access. The producer-consumer pattern appears everywhere in software: message queues, log pipelines, I/O buffers, and event-driven architectures all rely on the same principle of decoupling producers from consumers through a bounded buffer.

One thought on “Implementing the Producer-Consumer Algorithm in C++”

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.