- OS - Home
- OS - Overview
- OS - History
- OS - Evolution
- OS - Functions
- OS - Components
- OS - Structure
- OS - Architecture
- OS - Services
- OS - Properties
- Process Management
- Processes in Operating System
- States of a Process
- Process Schedulers
- Process Control Block
- Operations on Processes
- Process Suspension and Process Switching
- Process States and the Machine Cycle
- Inter Process Communication (IPC)
- Context Switching
- Threads
- Types of Threading
- Multi-threading
- System Calls
- Scheduling Algorithms
- Process Scheduling
- Types of Scheduling
- Scheduling Algorithms Overview
- FCFS Scheduling Algorithm
- SJF Scheduling Algorithm
- Round Robin Scheduling Algorithm
- HRRN Scheduling Algorithm
- Priority Scheduling Algorithm
- Multilevel Queue Scheduling
- Lottery Scheduling Algorithm
- Starvation and Aging
- Turn Around Time & Waiting Time
- Burst Time in SJF Scheduling
- Process Synchronization
- Process Synchronization
- Solutions For Process Synchronization
- Hardware-Based Solution
- Software-Based Solution
- Critical Section Problem
- Critical Section Synchronization
- Mutual Exclusion Synchronization
- Mutual Exclusion Using Interrupt Disabling
- Peterson's Algorithm
- Dekker's Algorithm
- Bakery Algorithm
- Semaphores
- Binary Semaphores
- Counting Semaphores
- Mutex
- Turn Variable
- Bounded Buffer Problem
- Reader Writer Locks
- Test and Set Lock
- Monitors
- Sleep and Wake
- Race Condition
- Classical Synchronization Problems
- Dining Philosophers Problem
- Producer Consumer Problem
- Sleeping Barber Problem
- Reader Writer Problem
- OS Deadlock
- Introduction to Deadlock
- Conditions for Deadlock
- Deadlock Handling
- Deadlock Prevention
- Deadlock Avoidance (Banker's Algorithm)
- Deadlock Detection and Recovery
- Deadlock Ignorance
- Resource Allocation Graph
- Livelock
- Memory Management
- Memory Management
- Logical and Physical Address
- Contiguous Memory Allocation
- Non-Contiguous Memory Allocation
- First Fit Algorithm
- Next Fit Algorithm
- Best Fit Algorithm
- Worst Fit Algorithm
- Buffering
- Fragmentation
- Compaction
- Virtual Memory
- Segmentation
- Buddy System
- Slab Allocation
- Overlays
- Free Space Management
- Locality of Reference
- Paging and Page Replacement
- Paging
- Demand Paging
- Page Table
- Page Replacement Algorithms
- Optimal Page Replacement Algorithm
- Belady's Anomaly
- Thrashing
- Storage and File Management
- File Systems
- File Attributes
- Structures of Directory
- Linked Index Allocation
- Indexed Allocation
- Disk Scheduling Algorithms
- FCFS Disk Scheduling
- SSTF Disk Scheduling
- SCAN Disk Scheduling
- LOOK Disk Scheduling
- I/O Systems
- I/O Hardware
- I/O Software
- I/O Programmed
- I/O Interrupt-Initiated
- Direct Memory Access
- OS Types
- OS - Types
- OS - Batch Processing
- OS - Multiprocessing
- OS - Hybrid
- OS - Monolithic
- OS - Zephyr
- OS - Nix
- OS - Linux
- OS - Blackberry
- OS - Garuda
- OS - Tails
- OS - Clustered
- OS - Haiku
- OS - AIX
- OS - Solus
- OS - Tizen
- OS - Bharat
- OS - Fire
- OS - Bliss
- OS - VxWorks
- OS - Embedded
- OS - Single User
- Miscellaneous Topics
- OS - Security
- OS Questions Answers
- OS - Questions Answers
- OS Useful Resources
- OS - Quick Guide
- OS - Useful Resources
- OS - Discussion
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
- Solution of the Problem
- Example Simulation
- Implementation in Programming Languages
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 −
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.