Introduction
Semaphores are powerful but low-level and error-prone: a single misplaced or forgotten sem_wait/sem_post call can cause deadlock or allow a race condition, and there is no language-level enforcement tying the semaphore to the data it protects. A monitor is a higher-level synchronization construct that packages shared data together with the procedures that operate on it, guaranteeing that at most one thread executes inside the monitor's procedures at a time, and provides condition variables for waiting on specific conditions.
Cricket analogy: Relying on every fielder to individually remember to return to position (like sem_wait/sem_post) is error-prone; a monitor is like a fielding captain who automatically resets positions between overs, bundling the field layout with the enforcement rule.
Explanation
A monitor automatically provides mutual exclusion: entering any procedure of the monitor implicitly acquires a lock, and leaving releases it, so the programmer cannot forget to unlock as they might with a raw semaphore. Because pure mutual exclusion is not always enough — a thread often needs to wait for some condition to become true (e.g., 'buffer not empty') — monitors add condition variables, each supporting two operations: wait(), which releases the monitor lock and suspends the calling thread until another thread signals the condition, and signal() (or notify), which wakes one thread waiting on that condition. Crucially, a condition variable is not a counter like a semaphore: if signal() is called with no thread waiting, the signal has no effect and is simply lost (it is not remembered for a future wait()), whereas a semaphore's sem_post() always increments its count. POSIX threads do not have a built-in 'monitor' keyword, but the standard idiom pthread_mutex_t + pthread_cond_t implements the same pattern: acquire the mutex, loop while (!condition) pthread_cond_wait(&cond, &mutex); (which atomically releases the mutex while waiting and re-acquires it on wake), then proceed, and use pthread_cond_signal/pthread_cond_broadcast to wake waiters after changing the condition.
Cricket analogy: Stepping into the striker's crease automatically means you're 'on strike' (implicit lock acquisition) and stepping out releases it; if the team must wait for rain to stop (a condition), they wait() and only resume when someone signals 'play has resumed.'
Example
#include <stdio.h>
#include <pthread.h>
#define BUFFER_SIZE 5
int buffer[BUFFER_SIZE];
int count = 0, in = 0, out = 0;
pthread_mutex_t mon_lock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t not_full = PTHREAD_COND_INITIALIZER;
pthread_cond_t not_empty = PTHREAD_COND_INITIALIZER;
void monitor_put(int item) {
pthread_mutex_lock(&mon_lock);
while (count == BUFFER_SIZE) { /* condition: buffer full */
pthread_cond_wait(¬_full, &mon_lock);
}
buffer[in] = item;
in = (in + 1) % BUFFER_SIZE;
count++;
pthread_cond_signal(¬_empty); /* wake a waiting consumer */
pthread_mutex_unlock(&mon_lock);
}
int monitor_get(void) {
pthread_mutex_lock(&mon_lock);
while (count == 0) { /* condition: buffer empty */
pthread_cond_wait(¬_empty, &mon_lock);
}
int item = buffer[out];
out = (out + 1) % BUFFER_SIZE;
count--;
pthread_cond_signal(¬_full); /* wake a waiting producer */
pthread_mutex_unlock(&mon_lock);
return item;
}Analysis
monitor_put and monitor_get together implement the same bounded-buffer behavior as the semaphore-based Producer-Consumer solution, but with a different structure: the mutex enforces that only one call to monitor_put/monitor_get runs at a time (mutual exclusion built into the abstraction), and pthread_cond_wait cleanly expresses 'block until this specific condition holds' without manually juggling counting semaphores. The while loop (rather than if) around the condition check is essential because pthread_cond_wait can return due to a spurious wakeup or because another thread got there first and changed the condition again, so the condition must always be re-checked after waking, before proceeding.
Cricket analogy: Two teams sharing a single practice net implement turn-taking not by counting slots like semaphores, but by one team calling monitor_put (dropping gear) and the other monitor_get (picking it up), with the coach's while-loop check re-verifying 'net actually free' before entering, since a false alarm can occur.
Key Takeaways
- A monitor bundles shared data, its operations, and synchronization into one construct, automatically enforcing mutual exclusion on entry/exit.
- Condition variables provide wait()/signal() so threads can block until an application-specific condition becomes true.
- Unlike a semaphore, a signal on a condition variable is lost if no thread is currently waiting — it is not counted or remembered.
- POSIX implements the monitor pattern with pthread_mutex_t plus pthread_cond_t and pthread_cond_wait/signal/broadcast.
- Always re-check the wait condition in a while loop after pthread_cond_wait returns, to guard against spurious wakeups and stale conditions.
Practice what you learned
1. What guarantee does a monitor provide automatically that a raw semaphore does not?
2. What happens if pthread_cond_signal is called on a condition variable that currently has no threads waiting on it?
3. Why should the condition in a pthread_cond_wait loop be checked with `while` rather than `if`?
4. In the POSIX monitor idiom, what does pthread_cond_wait(&cond, &mutex) do internally?
Was this page helpful?
You May Also Like
Mutex and Semaphores
Compare mutexes and binary/counting semaphores as tools for enforcing mutual exclusion and coordinating threads.
Classical Synchronization Problems
Study the Producer-Consumer, Readers-Writers, and Dining Philosophers problems and their semaphore-based solutions.
Interprocess Communication
How independent processes with separate address spaces exchange data and coordinate using OS-provided IPC mechanisms.