This simulator demonstrates two classic synchronization problems in operating systems. These problems illustrate fundamental challenges in concurrent programming: managing shared resources, preventing race conditions, avoiding deadlocks, and ensuring fair access to resources.
The Sleeping Barber Problem
Models coordination between a barber and customers in a barbershop with limited waiting chairs. Demonstrates mutex and semaphore usage for process synchronization.
The Cigarette Smokers Problem
Illustrates coordination between multiple processes waiting for specific resource combinations. Shows how to handle conditional synchronization.
Barber Station
Waiting Area (0/3 chairs)
📋 Event Log
Comprehensive Documentation
📖 What is the Sleeping Barber Problem?
The Sleeping Barber Problem is a classic inter-process communication and synchronization problem in computer science. It involves coordinating access to a shared resource (the barber) between multiple processes (customers) in a way that prevents race conditions, deadlocks, and ensures fair resource allocation.
Core Components:
- Barber: A shared resource that can serve one customer at a time
- Waiting Room: Limited number of chairs for customers to wait
- Customers: Multiple processes arriving at random times
- Mutual Exclusion: Only one customer can be served at a time
⚠️ Why is it Considered a Problem?
Without proper synchronization mechanisms, several critical issues can occur:
🔴 Race Conditions
Multiple customers might try to wake the barber simultaneously, or access waiting chairs concurrently, leading to unpredictable behavior and data corruption.
🟡 Deadlock
The barber might wait for customers while customers wait for the barber, creating a circular dependency where no progress can be made.
🟣 Starvation
Some customers might wait indefinitely if new customers keep arriving and the queue management isn't fair (though our implementation uses FIFO to prevent this).
🟠 Resource Waste
The barber sleeping when customers are waiting, or customers leaving because they can't determine if chairs are available, wastes CPU cycles and resources.
🌍 Real-World Applications
🏥 Hospital Emergency Room Management
Scenario: An emergency room has limited doctors (barbers) and a waiting area with limited seats (chairs).
- Patients arrive randomly at different times with varying urgency levels
- Doctors rest when no patients are waiting (sleeping barber)
- Patients must wait if doctors are busy and seats are available
- Patients leave if the waiting room is full (rejected customers)
- Synchronization needed: Proper queue management, doctor notification, patient tracking
💻 Web Server Request Processing
Scenario: A web server has worker threads (barbers) and a request queue with limited size (chairs).
- HTTP requests arrive from clients at unpredictable rates
- Worker threads sleep when idle to conserve resources
- Requests queue up when all workers are busy
- New requests rejected with 503 Service Unavailable if queue is full
- Synchronization needed: Thread pool management, request queuing, load balancing
🖨️ Print Spooler System
Scenario: A network printer (barber) serves multiple computers (customers) with a print queue (waiting room).
- Print jobs arrive from various users throughout the day
- Printer enters sleep mode when no jobs are queued (energy saving)
- Jobs wait in queue when printer is busy with current job
- Jobs rejected if spooler buffer is full
- Synchronization needed: Queue management, printer wake-up, job status tracking
Synchronization Mechanisms
The Sleeping Barber Problem can be solved using various synchronization primitives. Here are the most common approaches:
1️⃣ Semaphore-Based Solution
Uses three semaphores to coordinate between barber and customers:
// Semaphore declarations
Semaphore customers = 0; // Number of customers waiting
Semaphore barber = 0; // Barber ready to cut hair
Semaphore mutex = 1; // Protect waiting count access
int waiting = 0; // Number of customers waiting
int chairs = N; // Number of waiting chairs
// Customer process
customerArrival() {
mutex.wait(); // Lock
if (waiting < chairs) {
waiting++; // Sit in waiting room
mutex.signal(); // Unlock
customers.signal(); // Wake barber if sleeping
barber.wait(); // Wait for barber
getHaircut();
} else {
mutex.signal(); // Unlock
leaveShop(); // No chairs, leave
}
}
// Barber process
barberProcess() {
while (true) {
customers.wait(); // Sleep until customer arrives
mutex.wait(); // Lock
waiting--; // Customer leaving waiting room
mutex.signal(); // Unlock
barber.signal(); // Ready to cut hair
cutHair(); // Serve customer
}
}mutexensures atomic access to shared waiting countcustomerssignals barber when customer arrivesbarbersignals customer when ready to serve- Prevents race conditions through mutual exclusion
- Avoids deadlock with proper signal ordering
2️⃣ Monitor-Based Solution
Uses a monitor with condition variables for cleaner synchronization:
monitor BarberShop {
int waiting = 0;
int chairs = N;
condition customerReady, barberReady;
procedure customerArrival() {
if (waiting < chairs) {
waiting++;
signal(customerReady); // Wake barber
wait(barberReady); // Wait for service
waiting--;
} else {
// Shop full, leave
return;
}
}
procedure barberService() {
if (waiting == 0) {
wait(customerReady); // Sleep until customer
}
signal(barberReady); // Ready to serve
cutHair();
}
}3️⃣ This Simulator's Approach (Event-Driven State Machine)
Our implementation uses React state management with careful synchronization:
// State management
const [customers, setCustomers] = useState<Customer[]>([]);
const [barberStatus, setBarberStatus] = useState('sleeping');
const processingRef = useRef(false); // Prevent concurrent processing
// Customer arrival (separate interval)
useEffect(() => {
const interval = setInterval(() => {
if (Math.random() * 100 < arrivalRate) {
setCustomers(prev => {
if (prev.filter(c => c.status === 'waiting').length >= chairs) {
logEvent('Customer rejected - shop full');
return prev; // Shop full
}
logEvent('Customer arrived');
return [...prev, { id: nextId++, status: 'waiting' }];
});
}
}, speed * 1.5);
return () => clearInterval(interval);
}, [arrivalRate, speed]);
// Barber processing (separate interval)
useEffect(() => {
const interval = setInterval(() => {
if (processingRef.current) return; // Guard against concurrent
processingRef.current = true;
setCustomers(prev => {
const waiting = prev.filter(c => c.status === 'waiting');
const serving = prev.filter(c => c.status === 'getting-haircut');
if (serving.length > 0) {
// Finish current customer
logEvent('Customer finished');
setBarberStatus(waiting.length > 0 ? 'idle' : 'sleeping');
return prev.filter(c => c.id !== serving[0].id);
}
if (waiting.length > 0 && barberStatus !== 'cutting') {
// Start serving next customer
logEvent('Barber woke up, starting haircut');
setBarberStatus('cutting');
return prev.map(c =>
c.id === waiting[0].id
? { ...c, status: 'getting-haircut' }
: c
);
}
return prev;
});
processingRef.current = false;
}, speed);
return () => clearInterval(interval);
}, [speed, barberStatus]);processingRefprevents race conditions (acts like mutex)- Separate intervals for arrival and processing avoid conflicts
- Immutable state updates ensure predictable behavior
- FIFO queue (array) ensures fairness and prevents starvation
- Event logging provides visibility into synchronization
Implementation Best Practices
✅ Do's
- Use proper locking mechanisms (mutex, semaphores)
- Implement atomic operations for shared variables
- Design with clear state transitions
- Add timeout mechanisms to prevent indefinite waits
- Log events for debugging and monitoring
- Use FIFO queues for fairness
- Clean up resources properly (clear intervals)
❌ Don'ts
- Don't access shared state without synchronization
- Avoid busy-waiting (polling in tight loops)
- Don't create circular dependencies in locking
- Never assume execution order without guarantees
- Don't ignore race condition possibilities
- Avoid global mutable state when possible
- Don't forget to release locks/semaphores
💡 Key Takeaways
- The Sleeping Barber Problem teaches fundamental concepts in concurrent programming and resource management
- Proper synchronization is critical - without it, race conditions and deadlocks will occur
- Multiple solution approaches exist (semaphores, monitors, message passing) - choose based on your platform
- Real-world applications are everywhere: servers, databases, operating systems, embedded systems
- Modern solutions often use higher-level abstractions (async/await, channels) built on these primitives
- Testing concurrent systems is challenging - simulators like this help visualize and understand behavior
💡 Quick Reference - How it works:
- •The barber sleeps when no customers are waiting
- •Customers wake the barber when they arrive
- •If all chairs are full, new customers leave
- •Semaphores ensure proper synchronization and prevent race conditions
🔒 Mutual Exclusion
Ensures only one process accesses a shared resource at a time, preventing race conditions.
🚦 Semaphores
Synchronization primitives used to control access to shared resources and coordinate processes.
⚡ Race Conditions
Situations where the outcome depends on the timing of uncontrolled events, leading to unpredictable results.