Process Synchronization — Mutex, Semaphores, Monitors
1403 words
7 min read
Visual companion
Python
Type and operator map
Python Week 1: the first filter for runtime behavior
View
Revision summary
What this note is really saying
Short form
# Process Synchronization — Mutex, Semaphores, Monitors ## 🎯 Learning Objectives - Explain the critical section problem and its three requirements - Implement mutual exclusion using mutex locks and semaphores - Differentiate between counting and binary semaphores - Solve classic synchronization problems using semap...

Process Synchronization — Mutex, Semaphores, Monitors
🎯 Learning Objectives
- Explain the critical section problem and its three requirements
- Implement mutual exclusion using mutex locks and semaphores
- Differentiate between counting and binary semaphores
- Solve classic synchronization problems using semaphores and monitors
- Understand the ABA problem and spinlock tradeoffs
1. The Critical Section Problem
1.1 Intuition
When multiple threads access shared data concurrently, race conditions can produce incorrect results. The critical section is the code region where shared resources are accessed — we need to ensure only one thread enters it at a time.
1.2 Race Condition Example
cint counter = 0; // Thread 1 // Thread 2 counter++; counter++; // Load counter (0) // Load counter (0) // Increment to 1 // Increment to 1 // Store counter (1) // Store counter (1)
Expected:
counter = 2. Actual: counter = 1. Both threads read 0 before either writes 1.1.3 Critical Section Requirements
| Requirement | Meaning |
|---|---|
| Mutual Exclusion | Only one process can be in its critical section at a time |
| Progress | If no process is in its critical section, only processes that are outside their entry sections can decide who enters next |
| Bounded Waiting | There is a limit on how many times other processes can enter after a process requests entry |
(Diagram)
2. Mutex Locks
2.1 Intuition
A mutex (mutual exclusion) is like a bathroom key — only one person can hold it at a time. If you don't have the key, you wait outside.
2.2 Implementation
c#include <pthread.h> pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; void* thread_function(void* arg) { pthread_mutex_lock(&lock); // Entry section — acquire lock // Critical section shared_data++; pthread_mutex_unlock(&lock); // Exit section — release lock return NULL; }
2.3 Types of Mutex
| Type | Behavior |
|---|---|
| Normal (fast) | Deadlock if same thread locks twice |
| Recursive | Same thread can lock multiple times (counted) |
| Error-check | Returns error on invalid operations |
| Adaptive | Spins for a short time before sleeping |
2.4 Spinlock vs Mutex
(Diagram)
| Aspect | Spinlock | Mutex (Sleeping Lock) |
|---|---|---|
| CPU usage | High (busy-wait) | Low (sleeps) |
| Latency when lock released | Low | High (context switch) |
| Best for | Short CS | Long CS |
| Interrupt context | Safe | Not safe (can't sleep) |
3. Semaphores
3.1 Intuition
A semaphore is a more general synchronization primitive than a mutex. It's like a parking lot with S parking spots — cars (threads) enter until capacity is reached, then they wait.
3.2 Abstract Definition
A semaphore
S is an integer variable accessed through two atomic operations:wait(S)(P, down): DecrementS; ifS < 0, blocksignal(S)(V, up): IncrementS; ifS ≤ 0, wake a blocked process
c// Binary semaphore (like mutex) sem_t sem; sem_init(&sem, 0, 1); // Initialize to 1 sem_wait(&sem); // Entry section // Critical section sem_post(&sem); // Exit section // Counting semaphore — control access to N resources sem_t pool; sem_init(&pool, 0, N); // N available resources sem_wait(&pool); // Acquire one resource // Use resource sem_post(&pool); // Release resource
3.3 Binary vs Counting Semaphores
| Aspect | Binary Semaphore | Counting Semaphore |
|---|---|---|
| Value range | 0 or 1 | 0 to N |
| Initial value | Typically 1 | Any non-negative integer |
| Use case | Mutual exclusion | Resource pool management |
| Same as mutex? | Similar, no ownership tracking | — |
3.4 Worked Example: Producer-Consumer
c#define BUFFER_SIZE 10 int buffer[BUFFER_SIZE]; int in = 0, out = 0; sem_t empty; // Counts empty slots sem_t full; // Counts full slots sem_t mutex; // Binary semaphore for mutual exclusion void init() { sem_init(&empty, 0, BUFFER_SIZE); sem_init(&full, 0, 0); sem_init(&mutex, 0, 1); } void* producer(void* arg) { int item; while (1) { item = produce_item(); sem_wait(&empty); // Wait for empty slot sem_wait(&mutex); // Lock buffer buffer[in] = item; in = (in + 1) % BUFFER_SIZE; sem_post(&mutex); // Unlock buffer sem_post(&full); // Signal buffer is full } } void* consumer(void* arg) { int item; while (1) { sem_wait(&full); // Wait for full slot sem_wait(&mutex); // Lock buffer item = buffer[out]; out = (out + 1) % BUFFER_SIZE; sem_post(&mutex); // Unlock buffer sem_post(&empty); // Signal empty slot consume_item(item); } }
Tracing (buffer size 3, initial state):
| Step | Producer Action | Consumer Action | empty | full | mutex | Buffer |
|---|---|---|---|---|---|---|
| 0 | Initialize | — | 3 | 0 | 1 | [_, _, _] |
| 1 | wait(empty)→2, wait(mutex)→0 | — | 2 | 0 | 0 | — |
| 2 | Produce item A, post(mutex)→1 | — | 2 | 0 | 1 | [A, _, _] |
| 3 | post(full)→1 | — | 2 | 1 | 1 | [A, _, _] |
| 4 | — | wait(full)→0, wait(mutex)→0 | 2 | 0 | 0 | — |
| 5 | — | Consume A, post(mutex)→1 | 2 | 0 | 1 | [_, _, _] |
| 6 | — | post(empty)→3 | 3 | 0 | 1 | [_, _, _] |
4. Monitors
4.1 Intuition
A monitor is a high-level synchronization construct that automatically ensures mutual exclusion. Only one thread can be active inside a monitor at a time. Condition variables allow threads to wait for specific conditions inside the monitor.
4.2 Monitor Structure
c#include <pthread.h> typedef struct { int items; // Shared state pthread_mutex_t mutex; // For mutual exclusion pthread_cond_t cond; // Condition variable } Monitor; void monitor_function(Monitor* m) { pthread_mutex_lock(&m->mutex); // Automatically exclusive access while (!condition) { pthread_cond_wait(&m->cond, &m->mutex); // Release mutex, sleep } // Critical section pthread_cond_signal(&m->cond); // Wake one waiting thread pthread_mutex_unlock(&m->mutex); }
4.3 Condition Variables
| Function | Description |
|---|---|
pthread_cond_wait(cond, mutex) | Atomically release mutex and wait; on wake, reacquire mutex |
pthread_cond_signal(cond) | Wake one waiting thread |
pthread_cond_broadcast(cond) | Wake all waiting threads |
4.4 Monitor Example: Bounded Buffer
c#include <pthread.h> #define N 10 int buffer[N]; int count = 0, in = 0, out = 0; pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t not_full = PTHREAD_COND_INITIALIZER; pthread_cond_t not_empty = PTHREAD_COND_INITIALIZER; void put(int item) { pthread_mutex_lock(&mutex); while (count == N) pthread_cond_wait(¬_full, &mutex); // Wait until not full buffer[in] = item; in = (in + 1) % N; count++; pthread_cond_signal(¬_empty); // Wake consumer pthread_mutex_unlock(&mutex); } int get() { pthread_mutex_lock(&mutex); while (count == 0) pthread_cond_wait(¬_empty, &mutex); // Wait until not empty int item = buffer[out]; out = (out + 1) % N; count--; pthread_cond_signal(¬_full); // Wake producer pthread_mutex_unlock(&mutex); return item; }
5. Common Pitfalls
Pitfall 1: Semaphore ordering in Producer-Consumer
Mistake:
wait(mutex) then wait(empty) — if buffer is full, producer holds mutex and blocks. Consumer can't enter (mutex needed). Deadlock.
Correct: Always wait on resource semaphore first, then mutex.Pitfall 2: Forgotten signal/broadcast
Mistake: Thread calls
pthread_cond_wait but no other thread ever signals.
Result: The thread waits forever (deadlock).Pitfall 3: Spurious wakeups
Mistake: Using
if instead of while for condition check.c// WRONG: spurious wakeup breaks this if (count == 0) pthread_cond_wait(...); // Correct: always re-check condition while (count == 0) pthread_cond_wait(...);
6. 📐 Key Formulas / Concepts
| Concept | Definition | Use Case |
|---|---|---|
| Race condition | Concurrent access to shared data → unpredictable result | Prevent with synchronization |
| Mutex | Simple lock (acquire/release) | Mutual exclusion |
| Spinlock | Busy-wait lock | Short critical sections |
| Semaphore | Counter + wait/signal | Resource pools, signaling |
| Monitor | High-level construct with automatic mutex + CV | Structured synchronization |
| Condition variable | Wait/signal inside monitor | Complex condition waiting |
7. 📝 Practice Questions
Q1: What is a race condition? Give an example with two threads incrementing a counter.Answer: When two threads simultaneously executecounter++, the operation (load, increment, store) interleaves. Both read the same initial value, both increment to N+1, and both write N+1 — losing one increment. The fix is to use a mutex lock around the critical section. Q2: Explain the difference between mutex and semaphore.Answer: A mutex is a binary lock with ownership tracking (only the locking thread can unlock). A semaphore is a signaling mechanism that can be posted by any thread. A binary semaphore can be used like a mutex but without ownership. Counting semaphores manage multiple resources. Q3: In the producer-consumer problem, why must we check the buffer condition with a while loop instead of if?Answer: To handle spurious wakeups —pthread_cond_waitcan return even without a signal. Also, after waking, another thread may have changed the condition. The while loop re-checks the condition. Q4: Write pseudocode using semaphores to solve the readers-writers problem (reader preference).Answer:csem_t resource; // Binary semaphore for writer exclusion sem_t rmutex; // Protect read_count int read_count = 0; // Writer wait(resource); write(); signal(resource); // Reader wait(rmutex); read_count++; if (read_count == 1) wait(resource); signal(rmutex); read(); wait(rmutex); read_count--; if (read_count == 0) signal(resource); signal(rmutex);Q5: What is the difference between pthread_cond_signal and pthread_cond_broadcast?Answer:signalwakes exactly one waiting thread (if any).broadcastwakes all waiting threads. Usesignalwhen only one thread can proceed (e.g., a slot became available). Usebroadcastwhen multiple threads may need to re-check their conditions (e.g., after a state change).
8. 🔗 Cross-References
- Week 6 - Deadlocks: What happens when locking goes wrong
- Week 1 - Process Management: Processes vs threads sharing data
- BSCS3005 (C Programming): pthreads library usage Join Discord PreviousCPU Scheduling: RR, Priority, MLFQNextDeadlocks