concurrency
synchronization
semaphores
producer-consumer problem
parallel computing

Producer-consumer with sempahores

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

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_slots semaphore, initialized to the buffer capacity
  • a full_slots semaphore, initialized to 0
  • 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.

python
1import threading
2import time
3from collections import deque
4
5BUFFER_SIZE = 3
6ITEMS_PER_PRODUCER = 5
7
8buffer = deque()
9empty_slots = threading.Semaphore(BUFFER_SIZE)
10full_slots = threading.Semaphore(0)
11buffer_lock = threading.Lock()
12
13
14def producer(name: str) -> None:
15    for i in range(ITEMS_PER_PRODUCER):
16        item = f"{name}-item-{i}"
17        time.sleep(0.1)
18
19        empty_slots.acquire()
20        with buffer_lock:
21            buffer.append(item)
22            print(f"produced: {item:12} buffer={list(buffer)}")
23        full_slots.release()
24
25
26def consumer(name: str, total_items: int) -> None:
27    for _ in range(total_items):
28        full_slots.acquire()
29        with buffer_lock:
30            item = buffer.popleft()
31            print(f"consumed by {name}: {item:12} buffer={list(buffer)}")
32        empty_slots.release()
33        time.sleep(0.15)
34
35
36producer_threads = [
37    threading.Thread(target=producer, args=("P1",)),
38    threading.Thread(target=producer, args=("P2",)),
39]
40consumer_thread = threading.Thread(target=consumer, args=("C1", ITEMS_PER_PRODUCER * 2))
41
42for thread in producer_threads:
43    thread.start()
44consumer_thread.start()
45
46for thread in producer_threads:
47    thread.join()
48consumer_thread.join()

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_slots counts remaining capacity'
  • 'full_slots counts 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:

  1. wait on empty_slots
  2. enter the critical section
  3. append the item
  4. leave the critical section
  5. signal full_slots

For a consumer:

  1. wait on full_slots
  2. enter the critical section
  3. remove the item
  4. leave the critical section
  5. 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_slots tracks free capacity and full_slots tracks 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.Queue is usually the practical implementation, but semaphores are the right conceptual model

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track 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.

Browse interview questions

All Rights Reserved.