Operating System - Producer Consumer Problem



The Producer Consumer Problem is also known as the Bounded Buffer Problem. This is a classic synchronization problem often discussed in operating systems topic, to show the challenges of process synchronization and inter-process communication using real world example. This chapter explains the problem, its conditions, and how to solve it using semaphores.

Producer Consumer Problem Explained

In the Producer Consumer Problem, there are two types of processes, the producer process and the consumer process. Both processes share a common, fixed-size buffer ( or a storage area). The producer's job is to generate data (or products), put it into the buffer, and start again. At the same time, the consumer is consuming the data (i.e., removing it from the buffer), one at a time.

The image below illustrates the Producer Consumer Problem −

Producer Consumer Problem

The main challenge in this problem is to -

  • Ensure that the producer does not add data into the buffer if it is full.
  • Ensure that the consumer does not remove data from an empty buffer.

Solution to Producer Consumer Problem

To solve this problem, we can use counting semaphores to keep track of the number of empty and full slots in the buffer. We can use two semaphores:

  • empty − This semaphore counts the number of empty slots in the buffer. It is initialized to the size of the buffer.
  • full − This semaphore counts the number of full slots in the buffer. It is initialized to 0.
semaphore empty = N; // N is the size of the buffer
semaphore full = 0;
semaphore mutex = 1; // for mutual exclusion

The producer process can be defined as follows −

void producer() {
    while (true) {
        // Produce an item
        item = produce_item();
        // Wait for an empty slot
        wait(empty)
        // Acquire mutex for mutual exclusion
        wait(mutex);
        // Add the item to the buffer
        add_item_to_buffer(item);
        // Release mutex
        signal(mutex);
        // Signal that a new item is available
        signal(full);
    }
}

The consumer process can be defined as follows −

void consumer() {
    while (true) {
        // Wait for a full slot
        wait(full);
        // Acquire mutex for mutual exclusion
        wait(mutex);
        // Remove an item from the buffer
        item = remove_item_from_buffer();
        // Release mutex
        signal(mutex);
        // Signal that an empty slot is available
        signal(empty);
        // Consume the item
        consume_item(item);
    }
}

Here, the wait() operation decrements the semaphore value, and if the value is less than zero, the process is blocked. The signal() operation increments the semaphore value, and if there are any processes waiting, one of them is unblocked.

Example Simulation of Producer Consumer Problem

Consider a buffer of size 3. The following sequence of events illustrates how the producer and consumer processes interact using semaphores −

State 1 − There are 3 instances of buffer available, so the semaphore count is initialized to 3.

State 2 − The producer process P1 produces an item and wants to add it to the buffer. It performs a wait operation on the empty semaphore, decrementing the count to 2, and then adds the item to the buffer.

State 3 − Now, the consumer process P2 wants to consume an item from the buffer. It performs a wait operation on the full semaphore, decrementing the count to 0, and then removes the item from the buffer.

State 4 − The producer process P1 produces another item and adds it to the buffer, decrementing the empty semaphore count to 1.

This process continues, with the producer and consumer processes alternating their operations while ensuring that the buffer does not overflow or underflow.

Implementation in Programming Languages

The Producer Consumer Problem can be implemented in various programming languages using threading libraries that support semaphores. Here we provided implementation examples in CPP, Java, and Python.

Note − The solution uses multithreading to simulate the producers and consumers working together at the same time.

import threading
import time
import random

buffer = []
buffer_size = 5
empty = threading.Semaphore(buffer_size)
full = threading.Semaphore(0)
mutex = threading.Semaphore(1)

stop_event = threading.Event()

def producer():
    while not stop_event.is_set():
        item = random.randint(1, 100)
        # try to acquire an empty slot, but wake up periodically to check stop_event
        if not empty.acquire(timeout=0.5):
            continue
        # try to get mutex; if fail, give back the slot and retry
        if not mutex.acquire(timeout=0.5):
            empty.release()
            continue
        try:
            buffer.append(item)
            print(f'Produced: {item} (buffer size: {len(buffer)})')
        finally:
            mutex.release()
            full.release()
        time.sleep(random.random())
    print("Producer exiting")

def consumer():
    while not stop_event.is_set() or full._value > 0:  # try to consume remaining items
        # try to acquire an item; timeout to check stop_event periodically
        if not full.acquire(timeout=0.5):
            continue
        # try to get mutex; if fail, give back the 'full' permit and retry
        if not mutex.acquire(timeout=0.5):
            full.release()
            continue
        try:
            if buffer:
                item = buffer.pop(0)
                print(f'Consumed: {item} (buffer size: {len(buffer)})')
        finally:
            mutex.release()
            empty.release()
        time.sleep(random.random())
    print("Consumer exiting")

producer_thread = threading.Thread(target=producer)
consumer_thread = threading.Thread(target=consumer)

producer_thread.start()
consumer_thread.start()

# run for 5 seconds only
time.sleep(5)
stop_event.set()            # signal threads to stop

producer_thread.join()
consumer_thread.join()

print("Stopped cleanly.")

The output of the above Python code will be similar to the following −

Produced: 82 (buffer size: 1)
Produced: 55 (buffer size: 2)
Consumed: 82 (buffer size: 1)
Produced: 63 (buffer size: 2)
Producer exiting
Consumed: 55 (buffer size: 1)
Consumed: 63 (buffer size: 0)
Consumer exiting
Stopped cleanly.
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <atomic>
#include <random>
#include <chrono>

using namespace std::chrono_literals;

class BoundedBuffer {
public:
    BoundedBuffer(size_t capacity) : capacity_(capacity) {}

    // Try put with timeout. Returns true if inserted.
    bool try_put(int item, std::chrono::milliseconds timeout) {
        std::unique_lock<std::mutex> lk(mu_);
        if (!not_full_cv_.wait_for(lk, timeout, [this]{ return queue_.size() < capacity_ || stop_; }))
            return false; // timeout or stop
        if (stop_) return false;
        queue_.push(item);
        not_empty_cv_.notify_one();
        return true;
    }

    // Try take with timeout. Returns pair(found, item)
    std::pair<bool,int> try_take(std::chrono::milliseconds timeout) {
        std::unique_lock<std::mutex> lk(mu_);
        if (!not_empty_cv_.wait_for(lk, timeout, [this]{ return !queue_.empty() || stop_; }))
            return {false, 0}; // timeout
        if (queue_.empty()) return {false, 0}; // maybe stopped and empty
        int item = queue_.front();
        queue_.pop();
        not_full_cv_.notify_one();
        return {true, item};
    }

    void stop_and_notify() {
        {
            std::lock_guard<std::mutex> lk(mu_);
            stop_ = true;
        }
        not_full_cv_.notify_all();
        not_empty_cv_.notify_all();
    }

    bool empty() {
        std::lock_guard<std::mutex> lk(mu_);
        return queue_.empty();
    }

private:
    size_t capacity_;
    std::queue<int> queue_;
    std::mutex mu_;
    std::condition_variable not_full_cv_;
    std::condition_variable not_empty_cv_;
    bool stop_ = false;
};

int main() {
    const size_t BUFFER_SIZE = 5;
    BoundedBuffer buf(BUFFER_SIZE);
    std::atomic<bool> stop_flag(false);

    // Random generator
    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_int_distribution<int> dist(1, 100);
    std::uniform_int_distribution<int> sleepMillis(10, 300);

    // Producer
    std::thread producer([&](){
        while (!stop_flag.load()) {
            int item = dist(gen);
            // try to put, timeout so we can inspect stop_flag periodically
            if (buf.try_put(item, 500ms)) {
                std::cout << "Produced: " << item << std::endl;
            }
            std::this_thread::sleep_for(std::chrono::milliseconds(sleepMillis(gen)));
        }
        std::cout << "Producer exiting\n";
    });

    // Consumer: keep consuming until stop and buffer drained
    std::thread consumer([&](){
        while (!stop_flag.load() || !buf.empty()) {
            auto res = buf.try_take(500ms);
            if (res.first) {
                std::cout << "Consumed: " << res.second << std::endl;
            }
            std::this_thread::sleep_for(std::chrono::milliseconds(sleepMillis(gen)));
        }
        std::cout << "Consumer exiting\n";
    });

    // run for 5 seconds
    std::this_thread::sleep_for(5s);
    stop_flag.store(true);
    buf.stop_and_notify(); // wake condition_variable waiters

    producer.join();
    consumer.join();

    std::cout << "Stopped cleanly.\n";
    return 0;
}

The output of the above C++ code will be similar to the following −

Produced: 86
Consumed: 86
Produced: 45
Consumed: 45
Produced: 21
Consumed: 21
Producer exiting
Consumer exiting
Stopped cleanly.
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.locks.*;
import java.util.concurrent.atomic.AtomicBoolean;

public class ProducerConsumer {
    private static final int BUFFER_SIZE = 5;
    private final List<Integer> buffer = new LinkedList<>();
    private final Semaphore empty = new Semaphore(BUFFER_SIZE);
    private final Semaphore full = new Semaphore(0);
    private final ReentrantLock mutex = new ReentrantLock();
    private final AtomicBoolean stopFlag = new AtomicBoolean(false);
    private final Random rnd = new Random();

    public void startDemo() throws InterruptedException {
        Thread producer = new Thread(this::producer);
        Thread consumer = new Thread(this::consumer);

        producer.start();
        consumer.start();

        Thread.sleep(5000); // run for 5 seconds
        stopFlag.set(true);

        // wake threads that may be blocked by doing a tryAcquire with timeout logic in the run loops.
        // join threads
        producer.join();
        consumer.join();

        System.out.println("Stopped cleanly.");
    }

    private void producer() {
        try {
            while (!stopFlag.get()) {
                int item = rnd.nextInt(100) + 1;
                // try to acquire an empty slot but timeout so we can check stopFlag periodically
                if (!empty.tryAcquire(500, TimeUnit.MILLISECONDS)) {
                    continue;
                }
                boolean locked = false;
                try {
                    locked = mutex.tryLock(500, TimeUnit.MILLISECONDS);
                    if (!locked) {
                        // couldn't get mutex, return permit
                        empty.release();
                        continue;
                    }
                    buffer.add(item);
                    System.out.println("Produced: " + item + " (buffer size: " + buffer.size() + ")");
                    full.release();
                } finally {
                    if (locked) mutex.unlock();
                }
                Thread.sleep(rnd.nextInt(300)); // random small sleep
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        System.out.println("Producer exiting");
    }

    private void consumer() {
        try {
            // continue while not stopped OR there are items still represented by 'full' permits
            while (!stopFlag.get() || full.availablePermits() > 0) {
                if (!full.tryAcquire(500, TimeUnit.MILLISECONDS)) {
                    continue;
                }
                boolean locked = false;
                try {
                    locked = mutex.tryLock(500, TimeUnit.MILLISECONDS);
                    if (!locked) {
                        // couldn't get mutex, return permit
                        full.release();
                        continue;
                    }
                    if (!buffer.isEmpty()) {
                        int item = buffer.remove(0);
                        System.out.println("Consumed: " + item + " (buffer size: " + buffer.size() + ")");
                        empty.release();
                    } else {
                        // nothing to consume (race), release permits accordingly
                        full.release();
                    }
                } finally {
                    if (locked) mutex.unlock();
                }
                Thread.sleep(rnd.nextInt(300));
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        System.out.println("Consumer exiting");
    }

    public static void main(String[] args) throws InterruptedException {
        new ProducerConsumer().startDemo();
    }
}

The output of the above Java code will be similar to the following −

Produced: 42 (buffer size: 1)
Consumed: 42 (buffer size: 0)
Produced: 87 (buffer size: 1)
Consumed: 87 (buffer size: 0)
Produced: 15 (buffer size: 1)
Producer exiting
Consumed: 15 (buffer size: 0)
Consumer exiting

Conclusion

The Producer Consumer Problem shows the challenges of accessing a shared resource (the buffer) by multiple processes (the producer and consumer) while ensuring data consistency and synchronization. This is ensured by using semaphores to manage the state of the buffer (full and empty) and a mutex to provide mutual exclusion during buffer access. The provided implementations in Python, C++, and Java demonstrate how different programming languages can be used to solve this classic synchronization problem effectively.

Advertisements