Loading learning content...
Consider this scenario: three smokers sit at a table, each with an infinite supply of one of three ingredients needed to make and smoke a cigarette. One smoker has tobacco, another has paper, and the third has matches. An agent repeatedly places two random ingredients on the table. The smoker who possesses the third ingredient takes the two on the table, makes a cigarette, and smokes it. The process then repeats.
Introduced by Suhas Patil in 1971, the Cigarette Smokers Problem is deceptively simple to state but remarkably difficult to solve with semaphores alone. In fact, Patil designed it specifically to demonstrate limitations of the semaphore abstraction—showing that some synchronization patterns cannot be expressed cleanly with basic semaphores.
This problem models real-world scenarios where processes wait for specific combinations of resources rather than individual resources—matching problems in resource allocation, workflow coordination, and distributed systems.
By the end of this page, you will:
• Understand why the Cigarette Smokers Problem is fundamentally different from producer-consumer patterns • Recognize why naive semaphore solutions fail • Learn the "pusher" pattern that enables correct semaphore-based solutions • Analyze the theoretical significance: what this problem reveals about semaphore limitations • Connect the pattern to real-world resource matching and workflow systems
The Scenario:
Three ingredients are required to make and smoke a cigarette:
Three smoker processes exist:
One agent process repeatedly:
Constraint: The agent process cannot be modified. It simply places two ingredients and signals. The solution must coordinate the smokers correctly.
| Agent Places | Smoker with Missing Resource | That Smoker Has | Creates Complete Set? |
|---|---|---|---|
| Tobacco + Paper | Smoker C (has matches) | Matches | ✓ Yes: T + P + M |
| Tobacco + Matches | Smoker B (has paper) | Paper | ✓ Yes: T + M + P |
| Paper + Matches | Smoker A (has tobacco) | Tobacco | ✓ Yes: P + M + T |
Correctness Requirements:
Safety:
Liveness:
The Key Challenge:
Unlike simple producer-consumer, the smokers cannot simply wait for "any two items." Each smoker needs a specific pair. The matching logic—determining which smoker should wake up—is the crux of the problem.
The agent uses three different signals (e.g., three different semaphore posts) to indicate which pair of ingredients was placed. But a smoker needs to determine from two signals that their specific pair is present. This conditional wakeup based on a combination is what makes the problem hard with basic semaphores.
Let's examine why straightforward semaphore approaches don't work. This analysis reveals the core difficulty.
Naive Approach 1: One Semaphore Per Ingredient
The obvious first idea: have the agent signal a semaphore for each ingredient placed. Each smoker waits on the two semaphores for the ingredients they need.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
// BROKEN: Naive per-ingredient semaphore approach sem_t tobacco; // Signaled when tobacco is placedsem_t paper; // Signaled when paper is placedsem_t matches; // Signaled when matches is placed // Smoker A has tobacco, needs paper + matchesvoid* smoker_a(void* arg) { while (1) { sem_wait(&paper); // Wait for paper sem_wait(&matches); // Wait for matches make_cigarette(); smoke(); signal_agent_done(); }} // Smoker B has paper, needs tobacco + matchesvoid* smoker_b(void* arg) { while (1) { sem_wait(&tobacco); // Wait for tobacco sem_wait(&matches); // Wait for matches make_cigarette(); smoke(); signal_agent_done(); }} // Agent places two ingredientsvoid* agent(void* arg) { while (1) { int choice = random() % 3; switch (choice) { case 0: // Place tobacco + paper sem_post(&tobacco); sem_post(&paper); break; case 1: // Place tobacco + matches sem_post(&tobacco); sem_post(&matches); break; case 2: // Place paper + matches sem_post(&paper); sem_post(&matches); break; } wait_for_smoker(); }}When agent places tobacco + paper (Smoker C should smoke):
tobacco, then posts paperpaper (was waiting for paper+matches)matches (which wasn't placed!)tobacco (was waiting for tobacco+matches)matches (same issue!)The fundamental problem is that semaphores don't support conditional waits on combinations. Each smoker needs to wait for a specific pair, but sem_wait only waits for a single semaphore. When multiple smokers compete for the same ingredient signal, the wrong smoker can grab it.
Naive Approach 2: Per-Smoker Semaphores
The agent could directly signal the appropriate smoker:
1234567891011121314151617181920212223242526272829303132333435363738
// This works, but violates the problem constraints sem_t smoker_a_can_go; // Signal smoker A (has tobacco)sem_t smoker_b_can_go; // Signal smoker B (has paper)sem_t smoker_c_can_go; // Signal smoker C (has matches) void* agent(void* arg) { while (1) { int choice = random() % 3; switch (choice) { case 0: // Place tobacco + paper → smoker C needs matches sem_post(&smoker_c_can_go); break; case 1: // Place tobacco + matches → smoker B needs paper sem_post(&smoker_b_can_go); break; case 2: // Place paper + matches → smoker A needs tobacco sem_post(&smoker_a_can_go); break; } wait_for_smoker(); }} void* smoker_a(void* arg) { while (1) { sem_wait(&smoker_a_can_go); make_cigarette(); smoke(); signal_agent_done(); }} // This WORKS but is considered "cheating" - the agent knows// which smoker to signal. The problem requires the matching// logic to be in the smoker/pusher code, not the agent.This solution works but violates the problem constraints: the agent must be unaware of which smoker needs which ingredients. The agent only knows which ingredients it placed, not which smoker should respond.
Why This Matters:
In real systems, the "agent" is often hardware or a fixed protocol that cannot embed application-specific logic. For example:
The matching logic must be in the consumer (smoker) side, not the producer (agent) side.
The correct solution introduces pusher threads that act as intermediaries. Pushers observe which ingredients are placed and, when they detect a complete pair, signal the appropriate smoker.
Key Insight:
We separate the problem into two stages:
Pushers provide the "conditional logic" that basic semaphores lack. Each ingredient has a pusher that:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
/* * The Cigarette Smokers Problem - Pusher Pattern Solution * * The key insight: introduce "pusher" threads that observe * individual ingredient signals and track state to determine * when a complete pair exists for a specific smoker. */ #include <semaphore.h>#include <pthread.h>#include <stdio.h>#include <stdbool.h> /* * Agent's semaphores - one per ingredient * These are signaled by the agent when it places ingredients. */sem_t tobacco_on_table;sem_t paper_on_table;sem_t matches_on_table; /* * Smoker semaphores - signaled when their pair is complete */sem_t smoker_with_tobacco; // Needs paper + matchessem_t smoker_with_paper; // Needs tobacco + matchessem_t smoker_with_matches; // Needs tobacco + paper /* * Agent synchronization */sem_t agent_ready; /* * Shared state: tracks what's currently on the table * Protected by mutex */pthread_mutex_t table_mutex = PTHREAD_MUTEX_INITIALIZER;bool has_tobacco = false;bool has_paper = false;bool has_matches = false; /* ============================================ * PUSHER THREADS * ============================================ * Each pusher wakes when its ingredient arrives, * checks if a complement is present, and signals * the appropriate smoker if so. */ void* tobacco_pusher(void* arg) { while (1) { // Wait for tobacco to be placed sem_wait(&tobacco_on_table); pthread_mutex_lock(&table_mutex); if (has_paper) { // Tobacco + Paper on table → Smoker with matches can smoke has_paper = false; // Consume the paper sem_post(&smoker_with_matches); } else if (has_matches) { // Tobacco + Matches on table → Smoker with paper can smoke has_matches = false; // Consume the matches sem_post(&smoker_with_paper); } else { // Neither complement present yet - record tobacco and wait has_tobacco = true; } pthread_mutex_unlock(&table_mutex); } return NULL;} void* paper_pusher(void* arg) { while (1) { sem_wait(&paper_on_table); pthread_mutex_lock(&table_mutex); if (has_tobacco) { // Paper + Tobacco on table → Smoker with matches can smoke has_tobacco = false; sem_post(&smoker_with_matches); } else if (has_matches) { // Paper + Matches on table → Smoker with tobacco can smoke has_matches = false; sem_post(&smoker_with_tobacco); } else { // Record paper and wait has_paper = true; } pthread_mutex_unlock(&table_mutex); } return NULL;} void* matches_pusher(void* arg) { while (1) { sem_wait(&matches_on_table); pthread_mutex_lock(&table_mutex); if (has_tobacco) { // Matches + Tobacco on table → Smoker with paper can smoke has_tobacco = false; sem_post(&smoker_with_paper); } else if (has_paper) { // Matches + Paper on table → Smoker with tobacco can smoke has_paper = false; sem_post(&smoker_with_tobacco); } else { // Record matches and wait has_matches = true; } pthread_mutex_unlock(&table_mutex); } return NULL;} /* ============================================ * SMOKER THREADS * ============================================ * Each smoker waits on their personal semaphore, * which is signaled by a pusher when their pair * is complete. */ void* smoker_with_tobacco_thread(void* arg) { while (1) { sem_wait(&smoker_with_tobacco); printf("Smoker with tobacco: making cigarette...\n"); make_cigarette(); sem_post(&agent_ready); // Signal agent can place more printf("Smoker with tobacco: smoking...\n"); smoke(); } return NULL;} void* smoker_with_paper_thread(void* arg) { while (1) { sem_wait(&smoker_with_paper); printf("Smoker with paper: making cigarette...\n"); make_cigarette(); sem_post(&agent_ready); printf("Smoker with paper: smoking...\n"); smoke(); } return NULL;} void* smoker_with_matches_thread(void* arg) { while (1) { sem_wait(&smoker_with_matches); printf("Smoker with matches: making cigarette...\n"); make_cigarette(); sem_post(&agent_ready); printf("Smoker with matches: smoking...\n"); smoke(); } return NULL;} /* ============================================ * AGENT THREAD * ============================================ * Randomly places two ingredients, then waits. * This code is "fixed" - cannot be modified * per problem constraints. */ void* agent_thread(void* arg) { while (1) { sem_wait(&agent_ready); int choice = random() % 3; switch (choice) { case 0: // Place tobacco + paper printf("Agent: placing tobacco and paper\n"); sem_post(&tobacco_on_table); sem_post(&paper_on_table); break; case 1: // Place tobacco + matches printf("Agent: placing tobacco and matches\n"); sem_post(&tobacco_on_table); sem_post(&matches_on_table); break; case 2: // Place paper + matches printf("Agent: placing paper and matches\n"); sem_post(&paper_on_table); sem_post(&matches_on_table); break; } } return NULL;} /* ============================================ * MAIN - INITIALIZATION * ============================================ */int main() { // Initialize ingredient semaphores (start at 0) sem_init(&tobacco_on_table, 0, 0); sem_init(&paper_on_table, 0, 0); sem_init(&matches_on_table, 0, 0); // Initialize smoker semaphores (start at 0) sem_init(&smoker_with_tobacco, 0, 0); sem_init(&smoker_with_paper, 0, 0); sem_init(&smoker_with_matches, 0, 0); // Agent can start immediately sem_init(&agent_ready, 0, 1); // Create threads pthread_t pushers[3], smokers[3], agent; pthread_create(&pushers[0], NULL, tobacco_pusher, NULL); pthread_create(&pushers[1], NULL, paper_pusher, NULL); pthread_create(&pushers[2], NULL, matches_pusher, NULL); pthread_create(&smokers[0], NULL, smoker_with_tobacco_thread, NULL); pthread_create(&smokers[1], NULL, smoker_with_paper_thread, NULL); pthread_create(&smokers[2], NULL, smoker_with_matches_thread, NULL); pthread_create(&agent, NULL, agent_thread, NULL); // Run forever pthread_join(agent, NULL); return 0;}The pushers provide conditional logic that semaphores lack:
• Each pusher tracks whether its ingredient arrived first or second • When an ingredient finds its complement already present, the pusher knows which pair is complete • The pusher then signals the specific smoker who needs that pair • This moves the matching logic from the agent to the middleware layer
Key invariant: At any time, at most one boolean (has_tobacco, has_paper, has_matches) is true, because the agent places exactly two ingredients.
Let's rigorously verify the pusher solution's correctness.
Property 1: Mutual Exclusion
Claim: Only one smoker smokes at a time.
Proof:
agent_ready before placing ingredientsagent_ready starts at 1, allowing one placementagent_readyProperty 2: Correct Smoker Selection
Claim: When the agent places ingredients X and Y, the smoker with ingredient Z (where Z is the missing one) is signaled.
Proof by case analysis:
Case: Agent places tobacco + paper
tobacco_on_table and paper_on_table are postedsmoker_with_matchesAlternatively, if paper pusher runs first:
smoker_with_matchesEither way, smoker_with_matches is signaled ✓
Symmetric analysis applies to other cases. ∎
| Agent Places | First Pusher | Sets | Second Pusher | Signals |
|---|---|---|---|---|
| T + P | tobacco_pusher | has_tobacco = true | paper_pusher | smoker_with_matches |
| T + P | paper_pusher | has_paper = true | tobacco_pusher | smoker_with_matches |
| T + M | tobacco_pusher | has_tobacco = true | matches_pusher | smoker_with_paper |
| T + M | matches_pusher | has_matches = true | tobacco_pusher | smoker_with_paper |
| P + M | paper_pusher | has_paper = true | matches_pusher | smoker_with_tobacco |
| P + M | matches_pusher | has_matches = true | paper_pusher | smoker_with_tobacco |
Property 3: No Deadlock
Claim: The system never reaches a state where all threads are blocked forever.
Proof:
agent_ready = 1, so agent can runagent_ready, and the cycle continuesAt any point between complete agent placements: exactly zero or one of (has_tobacco, has_paper, has_matches) is true. This holds because:
Patil designed the Cigarette Smokers Problem specifically to expose a limitation of the semaphore abstraction. Understanding this deepens appreciation for why monitors and other synchronization primitives exist.
The Core Limitation:
Semaphores excel at:
But semaphores struggle with conditional synchronization based on multiple conditions—waiting until a combination of states is true.
Why Can't Smokers Just Wait on Two Semaphores?
The naive approach has smokers wait on two semaphores (for their two missing ingredients). The problem is that sem_wait is atomic but doesn't support atomic wait on multiple semaphores simultaneously.
1234567891011121314
// What we WANT (but semaphores don't support):wait_for_all(paper, matches); // Atomic wait for BOTH // What semaphores give us:sem_wait(&paper); // Might succeedsem_wait(&matches); // Might block forever if matches not there // The problem: after getting paper, if matches isn't there,// we're stuck holding paper. Meanwhile, another smoker// might need paper but we've taken it. // This is analogous to the "two-army problem" in atomicity:// We can't atomically acquire two resources when we don't// control both resource's timing.What the Pusher Pattern Actually Demonstrates:
The pusher pattern shows that we can work around the limitation by:
But this feels like we're compensating for a missing primitive—and we are. Monitors with condition variables handle this more naturally:
1234567891011121314151617181920212223242526272829303132
// With monitors, the solution is more direct: monitor CigaretteSmokers { condition can_smoke[3]; // One per smoker int placed[3] = {0, 0, 0}; // Count of each ingredient on table procedure agent_places(ingredients) { for each i in ingredients: placed[i]++; // Wake appropriate smoker based on what's there if (placed[PAPER] > 0 && placed[MATCHES] > 0) signal(can_smoke[TOBACCO_SMOKER]); else if (placed[TOBACCO] > 0 && placed[MATCHES] > 0) signal(can_smoke[PAPER_SMOKER]); else if (placed[TOBACCO] > 0 && placed[PAPER] > 0) signal(can_smoke[MATCHES_SMOKER]); } procedure smoker_takes(smoker_id, ingredient1, ingredient2) { while (placed[ingredient1] == 0 || placed[ingredient2] == 0) wait(can_smoke[smoker_id]); placed[ingredient1]--; placed[ingredient2]--; }} // Note: This still has conditional logic, but the monitor's// encapsulation and condition variables make it cleaner.// We still need to know which smoker to signal - that's// fundamental to the problem, not an artifact of the solution.The Cigarette Smokers Problem teaches a meta-lesson:
Different synchronization primitives have different expressive power. Semaphores are powerful but not universal. When you find yourself building elaborate workarounds (like pushers), consider whether a different primitive (monitors, channels, actor model) might express the solution more directly.
In practice, choose the abstraction that matches your problem's natural structure.
Despite its abstract formulation, the Cigarette Smokers pattern appears in real systems:
1. Workflow Engines with Parallel Dependencies
In workflow systems, a task might require multiple asynchronous inputs before executing:
The "pusher" pattern translates to a join node in workflow engines that collects required inputs and triggers the downstream task only when all are ready.
2. Complex Event Processing (CEP)
CEP systems detect patterns across event streams. The Cigarette Smokers pattern represents conjunction patterns:
Pushers are analogous to event correlators in CEP engines that track partial matches and trigger downstream logic when complete.
3. Resource Matching in Scheduling
In some scheduling problems, a job requires multiple resources of specific types:
The coordinator must match available resources and dispatch jobs when complete resource sets are available.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354
"""Workflow Engine Join Node - Cigarette Smokers Pattern This models a workflow task that requires multiple async inputs.The JoinNode acts as a "pusher" - collecting inputs and triggeringthe downstream task when all required inputs are present.""" from typing import Dict, Set, Callableimport threading class JoinNode: """ Collects multiple asynchronous inputs and triggers action when all required inputs are present. """ def __init__(self, required_inputs: Set[str], action: Callable): self.required = required_inputs self.action = action self.received: Dict[str, any] = {} self.lock = threading.Lock() self.completed = threading.Event() def receive(self, input_name: str, value: any): """Called when an upstream task completes.""" with self.lock: if input_name not in self.required: return # Ignore unexpected inputs self.received[input_name] = value # Check if all inputs present (pusher logic) if set(self.received.keys()) == self.required: # All inputs present - trigger action (signal smoker) self._trigger() def _trigger(self): """Execute the downstream task with collected inputs.""" self.action(self.received) self.received.clear() # Reset for next execution self.completed.set() # Usage example:def generate_report(inputs): db_data = inputs["database_query"] api_data = inputs["api_response"] print(f"Generating report with DB:{db_data}, API:{api_data}") join = JoinNode({"database_query", "api_response"}, generate_report) # Async inputs arrive...join.receive("database_query", {"users": 1000}) # First inputjoin.receive("api_response", {"weather": "sunny"}) # Second input triggers actionWhen you see these characteristics, think Cigarette Smokers:
• Multiple independent event sources • Consumer needs a specific combination of events • Events arrive asynchronously in any order • Action should trigger only when complete combination exists
The solution is always some form of coordinator/aggregator that tracks partial state.
Several variations of the Cigarette Smokers Problem explore additional complexities:
1. Multiple Agents
What if multiple agents can place ingredients simultaneously? This introduces additional race conditions:
2. Non-Exclusive Smoking
In the original problem, only one smoker smokes at a time. A relaxed variant allows concurrent smoking:
3. Generalized to N Ingredients and M Smokers
With more ingredients and smokers, the combinatorics explode:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
"""Generalized Cigarette Smokers: N ingredients, M consumers Each consumer has a set of "owned" ingredients and needsall other ingredients to proceed.""" from typing import Set, List, Dictimport threading class GeneralizedMatcher: def __init__(self, ingredient_sets: Dict[str, Set[str]], all_ingredients: Set[str]): """ ingredient_sets: maps consumer ID to ingredients they OWN all_ingredients: complete set of all ingredients Each consumer NEEDS: all_ingredients - their_owned_set """ self.consumers = { consumer_id: all_ingredients - owned for consumer_id, owned in ingredient_sets.items() } self.on_table: Set[str] = set() self.lock = threading.Lock() self.consumer_signals = { cid: threading.Semaphore(0) for cid in self.consumers } def place_ingredients(self, ingredients: Set[str]): """Agent places a set of ingredients.""" with self.lock: self.on_table.update(ingredients) # Check each consumer for consumer_id, needed in self.consumers.items(): if needed.issubset(self.on_table): # This consumer can proceed self.on_table -= needed # Consume ingredients self.consumer_signals[consumer_id].release() return # Only one consumer per placement def wait_and_consume(self, consumer_id: str): """Consumer waits until their ingredients are ready.""" self.consumer_signals[consumer_id].acquire() # Consumer has now obtained their ingredients4. Timed Ingredients (Expiration)
In real systems, resources might expire or become stale:
5. Priority-Based Matching
Some smokers might have higher priority:
Each variation teaches something about real systems:
• Multiple agents → Concurrent producers, need stronger serialization • Non-exclusive → Throughput optimization under resource constraints • Generalized matching → Complex resource allocation problems • Timed resources → Dealing with real-time constraints and expiration • Priority matching → Fairness vs. efficiency tradeoffs
The Cigarette Smokers Problem reveals deep truths about synchronization primitives and their limitations. Let's consolidate the key lessons:
You now understand the Cigarette Smokers Problem—one of the most instructive synchronization problems ever designed. Its value lies not just in the solution, but in revealing why the problem is hard in the first place. Next, we explore Bounded Buffer Variations, where we'll see how the basic producer-consumer pattern extends to handle more complex requirements.