The Producer–Consumer (Bounded Buffer) Problem
The Producer–Consumer (Bounded Buffer) Problem
The producer–consumer problem is a classic synchronization problem that captures many real-world concurrency issues.
The idea is simple:
-
Producers generate data items and place them into a shared buffer
-
Consumers remove data items from the buffer and use them
-
The buffer has a fixed size (bounded)
Key Constraints
-
A producer must not insert into a full buffer
-
A consumer must not remove from an empty buffer
-
Producers and consumers must not corrupt shared data
-
The solution must avoid race conditions and deadlock
First Attempt: Using empty and full Semaphores
In our first attempt to solve the producer–consumer (bounded buffer) problem, we introduce two counting semaphores:
-
empty– indicates how many buffer slots are empty -
full– indicates how many buffer slots are full
These semaphores allow producers and consumers to coordinate access to the shared buffer.
Shared Buffer and Helper Routines
-
put()inserts an item into the buffer -
get()removes an item from the buffer -
The buffer is treated as a circular array
Semaphore-Based Solution
Semaphore Initialization
-
empty = MAX→ all buffer slots are free -
full = 0→ no items available to consume
Producer Code ( Major )
Consumer Code ( Major)
Execution Scenario: MAX = 1 (Single Buffer Slot)
Assume:
-
One producer
-
One consumer
-
Single CPU
Step 1: Consumer Runs First
-
Consumer calls
sem_wait(&full)(Line C1) -
Since
full = 0, it is decremented to –1 -
Consumer blocks and waits for data
✔ Correct behavior: consumer waits because buffer is empty
Step 2: Producer Runs
-
Producer calls
sem_wait(&empty)(Line P1) -
Since
empty = 1, it decrements to 0 and proceeds -
Producer inserts data using
put() -
Producer calls
sem_post(&full)(Line P3)-
fullbecomes 0 -
Consumer is woken up
-
Step 3: What Happens Next?
Two possible outcomes:
-
Producer continues running
-
Tries
sem_wait(&empty)again -
Blocks because
empty = 0
-
-
Consumer runs
-
Returns from
sem_wait(&full) -
Consumes the item
-
Signals
empty
-
✔ In both cases, behavior is correct
Why This Works (So Far)
-
Producer waits if buffer is full
-
Consumer waits if buffer is empty
-
Semaphores correctly coordinate access
-
Works for:
-
One producer, one consumer
-
Multiple producers and consumers
-
MAX = 1
-
Important Note
⚠️ This solution does not yet provide mutual exclusion
-
put()andget()access shared variables (fill,use) -
With
MAX > 1and multiple producers or consumers, race conditions can occur -
This limitation is addressed in the next step by adding a mutex semaphore
Key Takeaway
This first attempt correctly handles buffer availability, but not mutual exclusion.
It is a necessary but incomplete solution, laying the foundation for the fully correct producer–consumer implementation.
Why the First Attempt Fails When MAX > 1
So far, our solution using the empty and full semaphores works correctly only when there is a single producer and a single consumer, or when MAX = 1.
Now let’s consider a more realistic case:
-
MAX = 10(buffer has multiple slots) -
Multiple producers
-
Multiple consumers
At this point, a race condition occurs.
Where Is the Problem?
The problem is not with the semaphores empty and full.
The problem lies in the put() and get() functions, specifically in the shared variables:
These variables are accessed and modified by multiple threads without protection.
Understanding the Race Condition (Two Producers Case)
Let’s walk through a concrete example.
Initial State
-
fill = 0 -
Buffer is empty
-
Two producer threads: Pa and Pb
Step-by-Step Execution
Step 1: Producer Pa Runs
-
Pa enters
put() -
Executes:
-
Pa writes data into
buffer[0] -
Before Pa can execute
fill = fill + 1, it gets interrupted
Step 2: Producer Pb Runs
-
Pb also enters
put() -
fillis still 0 -
Pb executes:
-
Pb overwrites
buffer[0] -
The data written by Pa is lost
What Went Wrong?
-
Both producers read the same value of
fill -
They both wrote to the same buffer slot
-
Updates to
fillwere not atomic -
There was no mutual exclusion
Why Semaphores Alone Were Not Enough
The semaphores:
-
empty→ ensure there is space in the buffer -
full→ ensure there is data to consume
✔ They control when producers and consumers run
❌ They do not protect how shared data is accessed
Root Cause of the Race Condition
The
put()andget()functions form critical sections, but they are not protected by a lock.
Specifically:
-
buffer[] -
fill -
use
are shared variables accessed concurrently.
Key Insight for Students
Counting semaphores manage availability, not mutual exclusion.
To fix this problem, we must:
-
Introduce a mutex (binary semaphore)
-
Protect the
put()andget()operations -
Ensure only one producer or consumer manipulates buffer indices at a time
Summary
| Aspect | Status |
|---|---|
| Race condition present | ❌ Yes |
| Data overwrite possible | ❌ Yes |
| Semaphores sufficient alone | ❌ No |
| Mutual exclusion needed | ✅ Yes |
This realization leads directly to the next step:
👉 Adding a mutex semaphore to protect the critical section
Adding Mutual Exclusion: The Idea
We already identified the problem:
-
put()andget()access shared variables (buffer,fill,use) -
These are critical sections
-
Therefore, we add a binary semaphore (
mutex) to ensure mutual exclusion
So the intuition is correct:
“Only one thread at a time should execute
put()orget().”
Figure 31.11 does exactly that by surrounding the code with a mutex.
Why This Still Doesn’t Work: Deadlock
Even though mutual exclusion is added, the program can now deadlock.
Let’s see exactly how.
The Problematic Code Pattern (Figure 31.11)
Simplified view:
Consumer
Producer
Deadlock Scenario (Step-by-Step)
Assume:
-
Buffer is initially empty
-
One producer, one consumer
-
Single CPU
Step 1: Consumer Runs First
-
Consumer calls:
→ Acquires the mutex
-
Consumer then calls:
→
full == 0, so consumer blocks
→ Consumer is now sleeping
→ BUT it still holds the mutex
Step 2: Producer Runs
-
Producer calls:
→ Mutex is already held by the consumer
→ Producer blocks
And Now… Deadlock
At this point:
| Thread | State | Waiting For |
|---|---|---|
| Consumer | Blocked | full to be signaled |
| Producer | Blocked | mutex to be released |
Why this is fatal:
-
Consumer cannot release the mutex until
fullis posted -
Producer cannot post
fullbecause it cannot acquire the mutex -
No thread can make progress
👉 This is a circular wait — the defining condition of deadlock.
Why This Happens (Key Insight)
You must never sleep while holding a lock.
In this code:
-
sem_wait(&full)andsem_wait(&empty)can block -
The mutex is acquired before those blocking calls
-
This violates a fundamental concurrency rule
Summary
What We Tried
-
Added a mutex to protect shared data
-
Correct idea ✅
What Went Wrong
-
Threads block while holding the mutex
-
Creates a circular wait
-
Results in deadlock ❌
Core Lesson
Do not hold a mutex while performing a potentially blocking operation.
What Comes Next
The fix is simple but subtle:
-
Acquire
empty/fullfirst -
Acquire the mutex only around the actual critical section
That leads to Figure 31.12, the correct and deadlock-free solution.
At Last, a Working Solution: Correct Producer–Consumer Design
The deadlock in the previous attempt occurred because threads were holding a mutex while performing blocking operations (sem_wait on full or empty). To fix this, we must reduce the scope of the lock.
Key Idea
-
The mutex should protect only the true critical section
-
Blocking operations (
sem_wait,sem_post) should not be performed while holding the mutex
This leads to a correct and deadlock-free solution, shown in Figure 31.12.
Correct Producer Code
Correct Consumer Code
Why This Solution Works
1. No Deadlock
-
Threads never block while holding the mutex
-
sem_wait(&empty)andsem_wait(&full)occur before acquiring the mutex
2. Mutual Exclusion is Preserved
-
put()andget()are fully protected -
Only one thread can modify
buffer,fill, oruseat a time
3. Correct Ordering
-
Producers wait for empty slots
-
Consumers wait for full slots
-
Semaphores correctly coordinate access
Design Pattern to Remember
Use semaphores for resource counting (empty/full)
Use a mutex only for protecting shared data



Comments
Post a Comment