Monitors in Process Synchronization
Semaphores and locks get the job done, but they put a lot of responsibility on the programmer. You have to remember to call wait before entering a critical section and signal after leaving it. Miss either one and you either get a race condition or a deadlock.
Monitors came about as a cleaner, higher-level solution that takes some of that burden away by bundling the synchronization logic directly with the shared data it protects.
What a Monitor Actually Is
A monitor is essentially a protective wrapper around shared data and the operations that work on that data. Think of it as a room with a single door. Only one person can be inside at a time. Anyone else who wants in has to wait outside until the current occupant leaves. The room itself contains the shared resources and everything you can do with them.
From a programming perspective, a monitor is a module that contains:
- Shared variables that should not be touched directly from outside.
- Procedures or methods that provide the only way to access and modify those variables.
- Condition variables for managing situations where a process needs to wait for something specific before it can continue.
- Initialization code that sets up the shared data when the monitor is first created.
Code outside the monitor cannot touch its internal variables directly. The only way in is through the monitor's defined procedures, and only one process can be executing inside at any given moment.

Monitors implementation in process synchronization.
How Condition Variables Work Inside a Monitor
Condition variables are what allow processes to coordinate more precisely within a monitor. Two operations make this work:
| Operation | What It Does | Effect on the Monitor Lock |
|---|---|---|
| wait(condition) | The process cannot proceed because the condition it needs is not met. It goes to sleep and joins the condition's waiting queue. | Releases the monitor lock so other processes can enter and potentially change things. |
| signal(condition) | Another process has changed things so the condition might now be met. It wakes up one sleeping process from the condition's queue. | Depends on the policy: Hoare monitors give the lock to the woken process immediately. Mesa monitors let the signaler continue. |
This is cleaner than busy waiting, where a process would just keep looping and checking. Here, the process actually sleeps and only wakes up when it is relevant.
Implementation in C++
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <chrono>
class Monitor {
private:
std::mutex mtx; // Monitor lock
std::condition_variable not_full; // Condition: buffer has space
std::condition_variable not_empty; // Condition: buffer has items
std::queue<int> buffer;
const int MAX_SIZE = 5;
public:
// Producer procedure
void produce(int item, int producer_id) {
std::unique_lock<std::mutex> lock(mtx);
// Wait while buffer is full (Mesa style: use while, not if)
not_full.wait(lock, [this]() {
return (int)buffer.size() < MAX_SIZE;
});
buffer.push(item);
std::cout << "Producer " << producer_id
<< " added item " << item
<< ". Buffer size: " << buffer.size() << std::endl;
not_empty.notify_one(); // Signal: buffer is not empty
}
// Consumer procedure
int consume(int consumer_id) {
std::unique_lock<std::mutex> lock(mtx);
// Wait while buffer is empty
not_empty.wait(lock, [this]() {
return !buffer.empty();
});
int item = buffer.front();
buffer.pop();
std::cout << "Consumer " << consumer_id
<< " removed item " << item
<< ". Buffer size: " << buffer.size() << std::endl;
not_full.notify_one(); // Signal: buffer has space
return item;
}
};
Monitor monitor;
void producer(int id) {
for (int i = 1; i <= 5; i++) {
int item = id * 10 + i;
monitor.produce(item, id);
std::this_thread::sleep_for(std::chrono::milliseconds(200));
}
}
void consumer(int id) {
for (int i = 0; i < 5; i++) {
monitor.consume(id);
std::this_thread::sleep_for(std::chrono::milliseconds(300));
}
}
int main() {
std::cout << "Monitor-Based Synchronization" << std::endl;
std::cout << "------------------------------" << std::endl;
std::thread p1(producer, 1);
std::thread p2(producer, 2);
std::thread c1(consumer, 1);
std::thread c2(consumer, 2);
p1.join(); p2.join();
c1.join(); c2.join();
std::cout << "------------------------------" << std::endl;
std::cout << "All threads completed." << std::endl;
return 0;
}Output:
Monitor-Based Synchronization
------------------------------
Producer 1 added item 11. Buffer size: 1
Producer 2 added item 21. Buffer size: 2
Consumer 1 removed item 11. Buffer size: 1
Producer 1 added item 12. Buffer size: 2
Consumer 2 removed item 21. Buffer size: 1
Producer 2 added item 22. Buffer size: 2
...
------------------------------
All threads completed.Producers and consumers coordinate through the monitor without any explicit semaphore calls scattered around the code. The monitor handles all of that internally.
What Monitors Do Well
- Simplicity: No need to manually track wait and signal calls across different parts of the program. The synchronization is built into the structure itself.
- Encapsulation: Internal shared variables are hidden. External code cannot accidentally bypass synchronization by touching variables directly.
- Automatic mutual exclusion: You do not have to think about it. The monitor guarantees only one process is active inside at any given time.
- Condition variables: A proper way to handle waiting without wasting CPU cycles on busy loops.
- Modularity: Grouping related data and procedures together makes code easier to read, maintain, and reason about.
Where Monitors Fall Short
- Complex patterns involving multiple monitors or coordination across different shared resources can be awkward to express cleanly.
- Composing multiple monitors is not straightforward. Holding resources from two monitors simultaneously can easily cause deadlocks if locking order is not handled carefully.
- Performance overhead from lock acquisition. When many threads compete for the same monitor, contention becomes a bottleneck.
- Deadlocks are still possible if the internal logic is incorrect or if multiple monitors interact in uncontrolled ways.
Monitors vs Semaphores
| Aspect | Monitors | Semaphores |
|---|---|---|
| Abstraction Level | High-level. Synchronization is built into the structure. | Low-level. Programmer manages every wait() and signal() call. |
| Mutual Exclusion | Automatic. Guaranteed by the monitor itself. | Manual. Programmer must call wait() before and signal() after. |
| Data Protection | Encapsulated. External code cannot bypass procedures. | None. Any code can access the semaphore and shared data. |
| Error Prone | Less. Hard to misuse since the structure enforces the rules. | More. Forgetting signal() or misplacing wait() causes bugs. |
| Flexibility | Less. Constrained to the monitor's procedure-based model. | More. Can be used for diverse synchronization patterns. |
| Language Support | Java (synchronized), C# (lock), Python (with threading.Condition). | OS-level and available in most system programming languages. |
Monitors strike a good balance between safety and usability for a wide range of synchronization problems. They are not perfect, and for very performance-sensitive or complex scenarios you might need to drop down to semaphores or even hardware-level primitives. But for most everyday concurrent programming tasks, a monitor gives you the structure and guarantees you need without forcing you to micromanage every lock and unlock call.
