Session 3.3: High-Level Synchronization Tools
Exploring semaphores, mutex locks, monitors, and classic synchronization problems
Learning Objectives
By the end of this session, you will be able to:
- Understand semaphores and their operations
- Distinguish between counting and binary semaphores
- Implement mutex locks for mutual exclusion
- Explain monitors and condition variables
- Solve classic synchronization problems
- Analyze Producer-Consumer, Readers-Writers, and Dining Philosophers problems
Key Concept
High-level synchronization tools provide abstractions that make it easier to write correct concurrent programs while hiding the complexity of low-level synchronization mechanisms.
Semaphores
A semaphore is a synchronization primitive that maintains a counter and provides two atomic operations: wait() and signal().
Semaphore Operations
// Semaphore structure
typedef struct {
int value;
struct process *list; // waiting queue
} semaphore;
// Wait operation (P operation, acquire)
wait(semaphore *S) {
S->value--;
if (S->value < 0) {
// Add this process to S->list
block(); // Block the process
}
}
// Signal operation (V operation, release)
signal(semaphore *S) {
S->value++;
if (S->value <= 0) {
// Remove a process P from S->list
wakeup(P); // Wake up the process
}
}
Counting Semaphores
- Value can be any non-negative integer
- Used to control access to multiple resources
- Initial value = number of available resources
Binary Semaphores (Mutex)
- Value can only be 0 or 1
- Used for mutual exclusion
- Similar to a lock mechanism
Mutex Locks
Mutex (mutual exclusion) locks provide a simple mechanism for protecting critical sections by ensuring only one thread can hold the lock at a time.
Mutex Implementation
// Mutex structure
typedef struct {
boolean available;
struct process *owner;
} mutex_lock;
// Acquire the lock
acquire(mutex_lock *lock) {
while (!lock->available); // Busy wait
lock->available = false;
lock->owner = current_process;
}
// Release the lock
release(mutex_lock *lock) {
if (lock->owner == current_process) {
lock->available = true;
lock->owner = NULL;
}
}
// Usage example
mutex_lock mutex;
do {
acquire(&mutex);
// Critical Section
release(&mutex);
// Remainder Section
} while (true);
Mutex vs Binary Semaphore
- Ownership: Mutex has ownership concept, semaphore doesn't
- Release: Only lock owner can release mutex
- Priority: Mutex can support priority inheritance
- Recursion: Recursive mutexes allow same thread to acquire multiple times
Monitors
A monitor is a high-level synchronization construct that encapsulates shared data and the procedures that operate on it, ensuring mutual exclusion automatically.
Monitor Structure
monitor BoundedBuffer {
int buffer[N];
int count = 0;
int in = 0, out = 0;
condition full, empty;
procedure insert(item) {
if (count == N)
wait(full);
buffer[in] = item;
in = (in + 1) % N;
count++;
signal(empty);
}
procedure remove() {
if (count == 0)
wait(empty);
item = buffer[out];
out = (out + 1) % N;
count--;
signal(full);
return item;
}
}
Monitor Properties
- Automatic mutual exclusion
- Condition variables for synchronization
- Structured programming approach
- Compiler-enforced safety
Condition Variables
- wait(): Block until condition is true
- signal(): Wake up one waiting process
- broadcast(): Wake up all waiting processes
Producer-Consumer Problem
Problem Description
Producer processes generate data and place items in a shared buffer. Consumer processes remove items from the buffer. The buffer has finite capacity.
Semaphore Solution
// Shared data
int buffer[BUFFER_SIZE];
int in = 0, out = 0;
// Semaphores
semaphore mutex = 1; // Mutual exclusion
semaphore empty = BUFFER_SIZE; // Count of empty slots
semaphore full = 0; // Count of full slots
// Producer Process
do {
// Produce item
wait(empty); // Wait for empty slot
wait(mutex); // Enter critical section
buffer[in] = item;
in = (in + 1) % BUFFER_SIZE;
signal(mutex); // Exit critical section
signal(full); // Signal item available
} while (true);
// Consumer Process
do {
wait(full); // Wait for item
wait(mutex); // Enter critical section
item = buffer[out];
out = (out + 1) % BUFFER_SIZE;
signal(mutex); // Exit critical section
signal(empty); // Signal slot available
// Consume item
} while (true);
Readers-Writers Problem
Problem Description
Multiple processes want to access a shared database. Readers only read data, writers modify data. Allow concurrent reads, exclusive writes.
First Readers-Writers Solution
// Shared data
int readcount = 0;
// Semaphores
semaphore mutex = 1; // Protect readcount
semaphore wrt = 1; // Writer exclusion
// Reader Process
do {
wait(mutex);
readcount++;
if (readcount == 1) // First reader blocks writers
wait(wrt);
signal(mutex);
// Reading is performed
wait(mutex);
readcount--;
if (readcount == 0) // Last reader allows writers
signal(wrt);
signal(mutex);
} while (true);
// Writer Process
do {
wait(wrt);
// Writing is performed
signal(wrt);
} while (true);
Starvation Problem
In the first solution, writers may starve if readers keep arriving. The second solution gives priority to writers, but then readers may starve.
Dining Philosophers Problem
Problem Description
Five philosophers sit around a circular table. Each philosopher alternates between thinking and eating. To eat, a philosopher needs both adjacent forks (chopsticks).
Incorrect Solution (Causes Deadlock)
// Semaphores for forks
semaphore fork[5] = {1, 1, 1, 1, 1};
// Philosopher i
do {
wait(fork[i]); // Pick up left fork
wait(fork[(i+1) % 5]); // Pick up right fork
// Eat
signal(fork[(i+1) % 5]); // Put down right fork
signal(fork[i]); // Put down left fork
// Think
} while (true);
// DEADLOCK: All philosophers pick up left fork simultaneously!
Correct Solutions
Solution 1: Asymmetric Approach
// Even philosophers pick left fork first
// Odd philosophers pick right fork first
if (i % 2 == 0) {
wait(fork[i]); // Left first
wait(fork[(i+1) % 5]); // Then right
} else {
wait(fork[(i+1) % 5]); // Right first
wait(fork[i]); // Then left
}
Solution 2: Limit Concurrent Dining
// Allow only 4 philosophers to try eating simultaneously
semaphore dining_room = 4;
do {
wait(dining_room); // Enter dining room
wait(fork[i]);
wait(fork[(i+1) % 5]);
// Eat
signal(fork[(i+1) % 5]);
signal(fork[i]);
signal(dining_room); // Leave dining room
// Think
} while (true);
Session Summary
Key Takeaways:
- Semaphores: Provide a flexible synchronization mechanism with wait() and signal() operations
- Mutex Locks: Offer simpler mutual exclusion with ownership semantics
- Monitors: Encapsulate shared data and provide automatic synchronization
- Condition Variables: Allow processes to wait for specific conditions in monitors
- Classic Problems: Demonstrate common synchronization patterns and solutions
- Trade-offs: Compare different synchronization tools and strategies
Study Tips:
- Practice implementing semaphore solutions for different scenarios
- Understand the difference between counting and binary semaphores
- Work through the classic problems step by step
- Identify potential deadlock situations and prevention strategies
Next Session:
Session 3.4: Deadlock Fundamentals
System model, deadlock characterization, and resource allocation graphs