Producer-consumer with sempahores
Interview Questions practice on Codemia
Over 8,000 real interview questions from top companies, searchable by company and role.
Introduction
The producer-consumer problem is a classic concurrency exercise because it forces you to coordinate work between threads without losing data or corrupting shared state. Semaphores are a natural fit because they can model how many buffer slots are empty, how many are full, and when only one thread may touch the buffer structure itself.
What Semaphores Are Doing in This Pattern
A bounded-buffer producer-consumer design usually uses three synchronization primitives:
- an
empty_slotssemaphore, initialized to the buffer capacity - a
full_slotssemaphore, initialized to0 - a mutex or binary semaphore to protect the queue itself
The idea is simple.
A producer must wait until there is space, then lock the queue, push an item, unlock the queue, and signal that one more item is available.
A consumer does the reverse. It waits until an item exists, locks the queue, removes an item, unlocks the queue, and signals that one more slot is free.
That ordering matters. Counting semaphores track resource availability. The mutex protects the critical section where the shared buffer is actually modified.
A Runnable Python Example
Python's threading module includes both Semaphore and Lock, which are enough to implement the pattern directly.
This program uses a deque as the shared buffer. Producers block when the deque reaches the buffer size. Consumers block when the deque is empty. The lock ensures that queue operations and diagnostic output stay consistent.
Why the Three-Primitives Design Works
It is tempting to think that one mutex is enough. It is not.
A mutex can protect the queue from concurrent mutation, but it cannot express the resource counts that make the system block correctly. Without empty_slots, a producer would need to spin or poll to discover whether space exists. Without full_slots, a consumer would need to do the same when the queue is empty.
Semaphores remove that waste. Threads sleep until the relevant condition becomes true.
A useful mental model is:
- '
empty_slotscounts remaining capacity' - '
full_slotscounts available work' - the lock protects the actual data structure
Ordering Rules You Should Not Break
The most important correctness rule is to acquire and release in the right order.
For a producer:
- wait on
empty_slots - enter the critical section
- append the item
- leave the critical section
- signal
full_slots
For a consumer:
- wait on
full_slots - enter the critical section
- remove the item
- leave the critical section
- signal
empty_slots
If you signal before the queue mutation happens, another thread can wake up and observe an impossible state. If you hold the lock while sleeping on a semaphore, you can create unnecessary contention or outright deadlock.
When to Prefer a Queue Abstraction
In production Python code, queue.Queue is usually a better choice than writing this pattern manually. It already handles the bounded buffer, blocking behavior, and internal locking.
Still, understanding the semaphore version is valuable because it teaches the underlying mechanics. The same structure appears in operating systems, low-level runtimes, and interview-style concurrency problems.
Common Pitfalls
Using only a mutex protects shared memory but does not solve the empty-buffer and full-buffer waiting problem.
Releasing the counting semaphore before modifying the queue can wake another thread too early and produce inconsistent behavior.
Forgetting that printing is outside the concurrency problem also causes confusion. Diagnostic output can interleave unless you print while holding the same lock that protects the queue state.
Finally, avoid busy waiting. If a thread loops until the buffer changes, you have defeated the point of semaphores.
Summary
- the producer-consumer problem needs both mutual exclusion and resource counting
- '
empty_slotstracks free capacity andfull_slotstracks available items' - a mutex protects the shared buffer during append and remove operations
- correct acquire and release ordering is essential for safety
- in high-level Python applications,
queue.Queueis usually the practical implementation, but semaphores are the right conceptual model
Related reading
- Producer/consumer multithreading
- Programmatically determine which Java thread holds a lock
- Promise is blocking the thread
- promise.all inside a forEach loop — everything firing at once
- Promises - How to make asynchronous code execute synchronous without async / await?
- Proper handling of context data in libaio callbacks?
- Proper request with async/await in Node.JS
- Proper use of mutexes in Python
.png&w=3840&q=75)
Tackling System Design Interview Problems
A short course that equips you with the skills to approach system design interviews methodically.
Start the free courseTrack what you have practised
A free account saves your progress, solutions and study plan across every problem on Codemia.
Interview Questions practice on Codemia
Over 8,000 real interview questions from top companies, searchable by company and role.