Module 2 · Unit 6 · Lesson 5 of 7
Monitors
A synchronization abstraction provided by the programming
language rather than the operating system. A monitor
packages a shared data structure, the mutex that protects
it, and the procedures that operate on it into a single
unit. Mutual exclusion is enforced automatically — the
programmer never calls lock or
unlock. The monitor is the primitive most
modern languages use, because it is easier to use
correctly than a semaphore.
01 Introduction
Lessons 6.3 and 6.4 presented two synchronization primitives — the lock and the semaphore — that are provided by the operating system or its libraries. Both are powerful and both are error-prone. A lock must be acquired before every access to the shared state and released after every access, with no path through the code that forgets either. A semaphore must be signalled exactly as many times as it is waited on, in the right order, with no path that forgets or over-signals. In both cases, the correctness of the program depends on the programmer's discipline in following a protocol that the compiler cannot check.
The monitor is a different approach. It is
a synchronization abstraction provided by the
programming language rather than the operating
system. A monitor is a module — like a class in an
object-oriented language — that contains a shared data
structure, the procedures that operate on it, and the
synchronization that protects it. The compiler enforces the
mutual exclusion: a monitor procedure is executed by at
most one thread at a time, and the programmer never writes
an acquire or a release at all.
The monitor was introduced by Per Brinch Hansen in 1973 and
developed by Tony Hoare in 1974. It was the first widely
adopted high-level synchronization abstraction, and it
influenced the design of Java's synchronized
methods, C#'s lock statement, and the
synchronized blocks of many other languages.
It is the primitive that most modern programmers actually
use, even if they do not call it by that name.
This lesson presents the monitor abstraction. It defines the
structure of a monitor, explains how mutual exclusion is
enforced, and introduces the condition
variable — the mechanism a monitor uses to wait
for a condition to become true. It presents the
producer–consumer problem solved with a monitor, compares
the two semantics that a monitor's signal
operation can have (Hoare and Mesa), and explains why C and
POSIX do not provide monitors directly. The condition
variable itself is the subject of Lesson 6.6, which follows
this one; here we introduce it enough to write correct
monitor code.
Prerequisites
This lesson builds directly on Lessons 6.1–6.4. The race conditions of Lesson 6.1 motivate the problem; the critical-section abstraction of Lesson 6.2 provides the framework; the lock of Lesson 6.3 provides the implementation technique; and the semaphore of Lesson 6.4 provides the comparison point — a monitor solves the same problems as a semaphore, with a different programming model. If the difference between a mutex and a semaphore is unclear, review the comparison table in section 4.10 of Lesson 6.4 before continuing.
02 Learning objectives
By the end of this lesson you will be able to
- Define a monitor and describe the mutual exclusion that the language enforces automatically.
- Distinguish the monitor's implicit synchronization from the explicit lock and semaphore protocols.
- Use condition variables with a monitor to solve the bounded-buffer producer–consumer problem.
- Compare Hoare semantics (signal-and-wait) with Mesa semantics (signal-and-continue) and explain the consequences of each.
- Describe how modern languages implement monitors and why C and POSIX do not provide them directly.
This lesson contributes to U6.3, U6.4, and U6.5, and to course learning outcomes CLO2, CLO3, and CLO8.
03 Required reading
- Operating System Concepts — Chapter 6, Section 6.7, Monitors. The standard treatment, including the producer–consumer solution with condition variables and the discussion of Hoare versus Mesa semantics.
- Tanenbaum & Bos — Modern Operating Systems, Chapter 2, Section 2.3.7, Monitors. A complementary treatment that places the monitor in the historical context of the 1970s and includes the dining philosophers solution.
- OSTEP — Chapter 30, Condition Variables. OSTEP treats the condition variable as the primitive and builds the monitor-style solutions around it. Read this as a preview of Lesson 6.6 and for the producer–consumer solutions using condition variables.
- Hoare, C. A. R. — Monitors: An Operating System Structuring Concept. Communications of the ACM, 1974. The original paper on monitors and condition variables. Read the first three sections; the rest is more technical.
- Brinch Hansen, P. — Operating System Principles, Chapter 3. The original presentation of the monitor concept from a design perspective.
-
Java Language Specification — Section
17.1, Synchronization, and Section 8.4.3,
synchronizedMethods. The Java implementation of monitor semantics.
Reading strategy
Read the Silberschatz section first for the formal definition and the structure of a monitor. Then read the Hoare paper for the design rationale — it is short and surprisingly readable, and it explains why the condition variable was designed the way it was. Skim OSTEP Chapter 30 for the condition-variable solutions that you will examine in more detail in Lesson 6.6. Pay particular attention to the Hoare-versus-Mesa discussion; it is the most important design decision in the monitor abstraction, and it is the one most often misunderstood.
04 Core content
4.1 The monitor abstraction
A monitor is a programming-language construct that packages three things into a single unit:
- Shared data. The state that the monitor protects — a buffer, a counter, a queue, any data structure that requires synchronization.
- Procedures. The operations that read and modify the shared data. These are the only operations that external code can invoke.
- Initialization. Code that initializes the shared data when the monitor is created.
The essential property of a monitor is that at most one thread can be executing a monitor procedure at a time. The compiler or the runtime enforces this; the programmer does not write any code to make it happen. When a thread calls a monitor procedure, the runtime automatically acquires a lock on the monitor before the procedure body runs, and releases it when the procedure returns. If another thread is already inside a monitor procedure, the calling thread blocks until the monitor is free.
wait on a condition
variable, which suspends it and releases the monitor; a
thread that makes the condition true calls
signal, which wakes one of the waiters.
4.2 The difference from locks and semaphores
The essential difference between a monitor and the lower-level primitives is where the synchronization is enforced. With a lock or a semaphore, the synchronization is the programmer's responsibility: the programmer writes the acquire and release, or the wait and signal, and is responsible for every path through the code.
With a monitor, the synchronization is part of the abstraction. The compiler or runtime inserts the acquire and release around every monitor procedure, automatically and correctly. The programmer writes only the shared data and the operations on it.
| Aspect | Lock | Semaphore | Monitor |
|---|---|---|---|
| Who enforces mutual exclusion? | The programmer, by writing acquire and release | The programmer, by writing wait and post | The language runtime, automatically |
| Can the compiler check it? | No | No | Yes — mutual exclusion is structural |
| What if the programmer forgets? | Silent race or deadlock | Silent race or deadlock | Impossible — the runtime inserts it |
| Ownership semantics | Owned by the holder | No ownership | Owned by the entering thread |
| Waiting for a condition | Not directly supported | Via a separate semaphore | Via a condition variable |
| Provided by | OS / library | OS / library | Programming language |
The monitor's structural guarantee is a significant advantage: a whole class of errors — forgetting to release a lock, acquiring a lock twice, acquiring the wrong lock — becomes impossible by construction. This is why languages that provide monitors are widely regarded as easier to use for concurrent programming than C and C++.
4.3 Condition variables
Mutual exclusion alone is not enough to solve the problems of the previous two lessons. The producer–consumer problem requires a producer to wait until a slot is free, and a consumer to wait until an item is available. Both waits are for a condition that another thread will make true — not merely for the monitor to be free.
A monitor provides condition variables for
this purpose. A condition variable c is an
object with three operations:
c.wait()-
Release the monitor, suspend the calling thread, and add
it to the wait queue for
c. When the thread is woken (by asignalorbroadcast), it re-acquires the monitor and returns fromwait. The release-and-reacquire is automatic; the programmer writes onlyc.wait(). c.signal()-
Wake one thread waiting on
c, if any. If no thread is waiting, the signal has no effect — it is lost, unlike a semaphore's post, which would increment the value. c.broadcast()(alsoc.notify_all())-
Wake all threads waiting on
c. Used when the condition that has become true may allow more than one waiter to proceed.
The central difference between a condition variable and a
semaphore is that a condition variable has no
memory. A signal that arrives when no
thread is waiting has no effect. This is because the
condition variable is always used inside a
monitor, where the condition is a property of the shared
data: if the condition is true, a thread that later enters
the monitor will see it directly, without needing a
remembered signal. The signal is only a wakeup, not a
value.
A monitor may have any number of condition variables, each associated with a different condition on the shared data. In the producer–consumer solution, for example, we use one condition variable for "buffer is not full" and another for "buffer is not empty".
4.4 Producer–consumer with a monitor
The bounded-buffer producer–consumer problem, solved with a
monitor, is the classic example. The monitor holds the
buffer and its indices, and provides two procedures,
insert and remove:
monitor BoundedBuffer {
/* shared data */
int buffer[N];
int in = 0, out = 0, count = 0;
/* condition variables */
condition not_full;
condition not_empty;
procedure insert(int item) {
while (count == N)
not_full.wait();
buffer[in] = item;
in = (in + 1) % N;
count++;
not_empty.signal();
}
procedure remove() {
while (count == 0)
not_empty.wait();
int item = buffer[out];
out = (out + 1) % N;
count--;
not_full.signal();
return item;
}
}
A producer calls insert(item) and a consumer
calls remove(). Neither calls any
synchronization operation directly: the runtime acquires
the monitor's lock on entry to the procedure and releases
it on exit. The wait calls handle the cases
where the buffer is full or empty; the signal
calls notify a waiting thread that the condition has
changed.
Compare this to the semaphore solution from Lesson 6.4. The
monitor version is shorter, clearer, and has no explicit
locking. The synchronization is expressed entirely in terms
of the conditions the procedures must satisfy —
count < N to insert, count > 0
to remove — and the wait-loop for each condition is local
to the procedure that checks it. The semaphore version
required the programmer to reason about three separate
semaphore values and the order of the waits.
Always wait in a loop
The wait is written inside a
while loop, not an if. This is
essential. When a waiting thread is woken by a
signal, the condition it was waiting for
may no longer hold — another thread may have run first
and consumed the item, or the scheduler may have woken
it spuriously. Re-checking the condition after the wait
ensures that the thread proceeds only when the condition
is actually true. A wait inside an
if is one of the most common bugs in
monitor-style code.
4.5 Hoare versus Mesa semantics
When a thread calls c.signal(), one of the
waiters on c is woken. But there are two
possible semantics for what happens next, and the choice
between them has substantial consequences for how monitor
code must be written.
- Hoare semantics (signal-and-wait)
- The signalling thread immediately gives up the monitor to the woken thread, which runs as soon as the signaller suspends. The signaller is placed in a special queue and resumes when the woken thread leaves the monitor or waits again. The woken thread can be sure that the condition it was waiting for is still true when it wakes, because no other thread has had a chance to change it.
- Mesa semantics (signal-and-continue)
- The signalling thread continues executing, and the woken thread is moved to the monitor entry queue, where it must compete with new arrivals for the monitor. By the time the woken thread actually runs, other threads may have entered the monitor and changed the shared data. The condition it was waiting for may no longer hold.
wait must always be inside a
while loop under Mesa semantics.
Hoare semantics is simpler to reason about: a thread that
wakes from wait knows that the condition it
was waiting for is true, so an if would
suffice. But Hoare semantics is harder to implement — the
signaller must be suspended and resumed with the correct
priority — and it leads to more context switches, because
every signal that wakes a thread causes the
signaller to be suspended. For these reasons, virtually
every real implementation of monitors uses Mesa semantics.
The consequence is that every wait
must be inside a while loop. Under
Mesa semantics, the woken thread must re-check the
condition after it re-acquires the monitor, because another
thread may have consumed the item or filled the slot in the
meantime. This is why the producer–consumer code above
uses while (count == N) rather than
if (count == N). The while loop
is correct under both semantics; the if is
correct only under Hoare.
4.6 signal versus broadcast
A condition variable has two wakeup operations. The choice between them depends on how many waiters can proceed when the condition becomes true.
signal-
Wake exactly one waiter. Appropriate when the condition
that has become true allows exactly one waiter to make
progress. In the producer–consumer solution, one
inserted item allows one consumer to remove it, so a
signalis correct and more efficient than abroadcast. broadcast- Wake all waiters, each of which must re-check its condition. Appropriate when the condition that has become true may allow more than one waiter to proceed, or when the set of waiters is heterogeneous and the signaller does not know which one can proceed. The classic example is a change to a global state that different waiters are waiting on different parts of.
A broadcast is always correct — it wakes more
threads than necessary, and each re-checks its condition
and waits again if the condition is still false. But it
has a cost: it wakes threads that will immediately go back
to sleep, causing useless context switches and contention
on the monitor. On a system with many waiters, the
difference between signal and
broadcast can be substantial.
The general guidance is to use signal when the
condition is specific and only one waiter can proceed, and
broadcast when the condition is general or the
waiters are heterogeneous. The dining philosophers problem,
for example, uses broadcast because a
philosopher putting down a fork may allow either of two
neighbours to pick up a fork — and the signaller does not
know which of them will succeed.
4.7 Monitors in modern languages
The monitor concept as Hoare and Brinch Hansen described it — a language construct with a compiler that enforces mutual exclusion — is rare in its pure form. But the ideas appear in every modern language that supports concurrency.
-
Java. Every Java object has a monitor
associated with it. A method declared
synchronizedis executed as a monitor procedure: the JVM acquires the object's monitor on entry and releases it on exit. Asynchronizedblock acquires the monitor for the duration of the block. Java provideswait(),notify(), andnotifyAll()on every object, corresponding towait,signal, andbroadcast. Java uses Mesa semantics. -
C#. The
lockstatement acquires a monitor on a specified object for the duration of a block. TheMonitorclass providesWait,Pulse, andPulseAll. C# uses Mesa semantics. -
Python. The
threadingmodule providesLock,RLock, andCondition. AConditionis a condition variable, used together with a lock in the same pattern as a monitor. Python's GIL limits the concurrency of pure-Python code, but the abstractions are the same. -
Ruby. The
Monitorclass in the standard library provides a reentrant mutex and condition variables, used in the monitor pattern.
In each case, the language provides the monitor's key
guarantees: structural mutual exclusion for the protected
region, and condition variables for waiting on a
condition. The programmer does not write
acquire and release around every
critical section; the language inserts them.
4.8 Monitors in C and POSIX
C and POSIX do not provide monitors. There is no language-level construct that automatically acquires a lock on entry to a function and releases it on exit. The closest thing is the combination of a mutex and one or more condition variables, used together by convention:
/* The "monitor" is a struct containing a mutex and a condvar. */
typedef struct {
pthread_mutex_t mutex;
pthread_cond_t not_full;
pthread_cond_t not_empty;
int buffer[N];
int in, out, count;
} buffer_t;
/* Monitor procedure — the caller must lock the mutex first. */
void insert(buffer_t *b, int item) {
pthread_mutex_lock(&b->mutex);
while (b->count == N)
pthread_cond_wait(&b->not_full, &b->mutex);
b->buffer[b->in] = item;
b->in = (b->in + 1) % N;
b->count++;
pthread_cond_signal(&b->not_empty);
pthread_mutex_unlock(&b->mutex);
}
This is not a monitor in the language sense — the
programmer still writes the lock and unlock, and the
compiler does not check that every path does so correctly.
But the discipline is the same: the mutex protects the
shared data, and the condition variables handle waiting for
conditions. The pthread_cond_wait function
takes the mutex as an argument, because it must atomically
release the mutex and begin waiting — if the release and
the wait were not atomic, another thread could signal
between them and the signal would be lost.
The lesson for C programmers is that the monitor pattern is
available in C, but only as a convention the programmer
must follow. Nothing prevents a programmer from accessing
the shared data without holding the mutex, or from calling
pthread_cond_wait without the mutex, or from
forgetting to release the mutex on some path. The
structural guarantee of a true monitor is absent, and the
correctness of the program depends entirely on the
programmer's discipline. This is why C is considered
harder for concurrent programming than Java or C#, and why
the same patterns appear in C code with more ceremony and
more opportunities for error.
4.9 The dining philosophers with monitors
The dining philosophers problem is a second classic. Five philosophers sit at a round table, each alternating between thinking and eating. Between each pair of adjacent philosophers is a single fork, so there are five forks for five philosophers. To eat, a philosopher must pick up both forks adjacent to them.
The monitor solution uses a single monitor for the table, with a state variable for each philosopher and a condition variable per philosopher:
monitor Table {
enum { THINKING, HUNGRY, EATING } state[5];
condition self[5];
procedure pickup(int i) {
state[i] = HUNGRY;
test(i);
while (state[i] != EATING)
self[i].wait();
}
procedure putdown(int i) {
state[i] = THINKING;
test((i + 4) % 5); /* left neighbour */
test((i + 1) % 5); /* right neighbour */
}
procedure test(int i) {
if (state[(i + 4) % 5] != EATING &&
state[i] == HUNGRY &&
state[(i + 1) % 5] != EATING) {
state[i] = EATING;
self[i].signal();
}
}
}
A philosopher calls pickup(i) before eating
and putdown(i) after. The test
procedure checks whether the philosopher can eat (both
neighbours are not eating) and, if so, sets the state to
EATING and signals the philosopher's condition
variable. The pickup procedure sets the state
to HUNGRY, runs the test, and waits if it
cannot eat yet.
The solution avoids deadlock: the monitor serializes the
state changes, and a philosopher is allowed to eat only
when both neighbours are not eating. It also avoids
starvation in a weak sense — a philosopher who is hungry
and whose neighbours eventually put down their forks will
be signalled by the test calls in the
neighbours' putdown procedures. The problem is
examined in more detail in Unit 7, where deadlock is the
subject.
4.10 Common pitfalls
wait inside an if instead of a while
Under Mesa semantics, the condition may not hold when
the woken thread runs. The wait must always be inside a
while loop that re-checks the condition.
Signalling the wrong condition variable
A monitor may have several condition variables, one per condition. Signalling the wrong one wakes a thread that is waiting for a different condition, which will re-check and go back to sleep — or, worse, will proceed on an invalid assumption.
Forgetting to signal after changing the state
A condition variable has no memory. If a thread changes the state so that a condition becomes true and does not signal, the waiting thread sleeps forever.
Using broadcast when signal suffices
A broadcast wakes every waiter, even those that will
immediately go back to sleep. Use signal
when only one waiter can proceed, and reserve
broadcast for the cases where the
condition is general.
Holding the monitor while doing long work
The monitor is held for the duration of a monitor procedure. A procedure that does a long computation, or that blocks on I/O, serializes the entire monitor for that duration. Keep the procedures short.
Calling wait without the monitor held
In C, calling pthread_cond_wait without
holding the mutex is undefined behaviour. In Java,
calling wait() without synchronizing on
the object throws IllegalMonitorStateException.
Assuming Hoare semantics
Code that is correct under Hoare semantics may be incorrect under Mesa semantics, because the condition may change between the signal and the wakeup. Write code that is correct under Mesa, since that is what real languages provide.
Mixing monitor and lock patterns
A monitor's mutual exclusion is provided by the runtime; adding an explicit lock inside a monitor procedure that also acquires the same lock is a deadlock. Do not mix the two patterns.
05 Worked example
The following example traces the monitor-based producer–consumer solution through a sequence of operations, and shows the difference between Hoare and Mesa semantics by tracing the same sequence under both.
Producer–consumer with a monitor
The monitor has a buffer of N = 3 slots, a count, and two
condition variables: not_full and
not_empty. Initially the buffer is empty
(count = 0), and the wait queues are empty.
We trace the operations of one producer P and two consumers C1 and C2, under Mesa semantics, with the interleaving shown.
| Step | Thread | Operation | count | not_full queue | not_empty queue |
|---|---|---|---|---|---|
| 0 | — | initial | 0 | empty | empty |
| 1 | C1 | remove(): count == 0, not_empty.wait() | 0 | empty | C1 |
| 2 | C2 | remove(): count == 0, not_empty.wait() | 0 | empty | C1, C2 |
| 3 | P | insert(1): count < 3, insert, count++ | 1 | empty | C1, C2 |
| 4 | P | not_empty.signal() — C1 woken | 1 | empty | C2 |
| 5 | P | return from insert | 1 | empty | C2 |
| 6 | P | insert(2): count < 3, insert, count++ | 2 | empty | C2 |
| 7 | P | not_empty.signal() — C2 woken | 2 | empty | empty |
| 8 | P | return from insert | 2 | empty | empty |
| 9 | C1 | re-acquires monitor, re-checks count == 0 → false | 2 | empty | empty |
| 10 | C1 | remove(): item = 1, count-- | 1 | empty | empty |
| 11 | C1 | not_full.signal() — no waiters | 1 | empty | empty |
| 12 | C1 | return 1 | 1 | empty | empty |
| 13 | C2 | re-acquires monitor, re-checks count == 0 → false | 1 | empty | empty |
| 14 | C2 | remove(): item = 2, count-- | 0 | empty | empty |
| 15 | C2 | not_full.signal() — no waiters | 0 | empty | empty |
| 16 | C2 | return 2 | 0 | empty | empty |
The trace illustrates the essential points. At step 4, the
producer signals not_empty, which wakes C1.
Under Mesa semantics, the producer continues (steps 5–8),
inserting a second item before C1 runs. When C1 finally
re-acquires the monitor at step 9, it re-checks the
condition count == 0, which is now false
(count = 2), so it proceeds. If the code had used an
if instead of a while, C1 would
have run correctly here — but only by luck. With a
different interleaving — say, if C2 had run first and
consumed both items — C1's assumption would be wrong.
The same trace under Hoare semantics
Under Hoare semantics, the trace is different. At step 4,
when the producer signals not_empty, the
producer is immediately suspended and C1 runs
immediately, before the producer inserts the
second item. C1 removes the item, runs to completion, and
returns. The producer then resumes at step 5 and inserts
the second item.
| Step | Thread | Operation | count | not_full queue | not_empty queue |
|---|---|---|---|---|---|
| 0 | — | initial | 0 | empty | empty |
| 1 | C1 | remove(): count == 0, not_empty.wait() | 0 | empty | C1 |
| 2 | C2 | remove(): count == 0, not_empty.wait() | 0 | empty | C1, C2 |
| 3 | P | insert(1): count < 3, insert, count++ | 1 | empty | C1, C2 |
| 4 | P | not_empty.signal() — P suspends | 1 | empty | C2 |
| 5 | C1 | runs immediately; count == 0 → false | 1 | empty | C2 |
| 6 | C1 | remove(): item = 1, count-- | 0 | empty | C2 |
| 7 | C1 | not_full.signal() — no waiters | 0 | empty | C2 |
| 8 | C1 | return 1 | 0 | empty | C2 |
| 9 | P | resumes; returns from insert | 0 | empty | C2 |
| 10 | P | insert(2): count < 3, insert, count++ | 1 | empty | C2 |
| 11 | P | not_empty.signal() — P suspends | 1 | empty | empty |
| 12 | C2 | runs immediately; count == 0 → false | 1 | empty | empty |
| 13 | C2 | remove(): item = 2, count-- | 0 | empty | empty |
| 14 | C2 | not_full.signal() — no waiters | 0 | empty | empty |
| 15 | C2 | return 2 | 0 | empty | empty |
| 16 | P | resumes; returns from insert | 0 | empty | empty |
The two traces produce the same final state (both items
consumed, count = 0), but the intermediate states are
different, and the Hoare version performs more context
switches because the signaller is suspended and resumed
twice. Under Hoare semantics, C1 and C2 can rely on the
condition being true when they wake; under Mesa
semantics, they must re-check. The while
loop is correct under both, which is why it is the
recommended form regardless of the language's semantics.
Correctness argument for the monitor solution
Three properties must hold. Mutual
exclusion: the runtime guarantees that at most
one thread is inside a monitor procedure at a time, so
the buffer's data and indices are never accessed
concurrently. No insertion into a full
buffer: the insert procedure waits
while count == N, so a producer proceeds
only when there is a free slot. No removal from
an empty buffer: the remove
procedure waits while count == 0, so a
consumer proceeds only when an item is available.
Additionally, the signal calls ensure that
waiting threads are woken when the condition they are
waiting for becomes true: a producer signals
not_empty after inserting, and a consumer
signals not_full after removing. Since every
change to count is accompanied by the
appropriate signal, no waiter is left sleeping when the
condition becomes true.
The dining philosophers trace
A trace of the dining philosophers monitor for a single
philosopher's transition is instructive. Suppose all five
philosophers are thinking, and philosopher 0 calls
pickup(0). The monitor sets
state[0] = HUNGRY, then runs
test(0). Neighbours 4 and 1 are thinking, so
the test succeeds: state[0] = EATING and
self[0].signal() is called. Since philosopher
0 is the only thread currently in the monitor (having
entered pickup), there is no one waiting on
self[0], so the signal has no effect. The
wait loop in pickup is then checked:
state[0] == EATING, so the loop exits and
philosopher 0 begins to eat.
Now suppose philosopher 1 calls pickup(1)
while philosopher 0 is eating. The monitor sets
state[1] = HUNGRY and runs
test(1). Philosopher 0 is eating, so the
test fails: philosopher 1 cannot eat. The wait loop
blocks on self[1].wait(), adding philosopher
1 to the wait queue for self[1]. When
philosopher 0 later calls putdown(0), the
monitor sets state[0] = THINKING and calls
test(4) and test(1). The latter
succeeds, because philosopher 0 is no longer eating and
philosopher 2 is thinking; so state[1] = EATING
and self[1].signal() wakes philosopher 1.
Philosopher 1 re-checks its loop condition, sees
state[1] == EATING, and proceeds to eat.
This example illustrates why the monitor is a good fit
for the dining philosophers. The shared state — the
state of each philosopher — is protected by the monitor's
mutual exclusion, and the condition "philosopher i can
eat" is expressed directly as a test
procedure and a condition variable. The solution avoids
the deadlock that a naive fork-picking solution would
produce, because a philosopher is allowed to eat only
when both neighbours are not eating, and the state is
only modified inside the monitor.
06 Interactive activity
Activity · implement a monitor-style solution in C and in Java
C does not provide monitors directly, but the monitor pattern can be implemented with a mutex and one or more condition variables. Java provides monitors as a language construct. This activity lets you implement the producer–consumer solution in both languages and compare the two programming models. Work through the steps in your course VM.
-
Implement the producer–consumer in C using the monitor pattern.
Write a C program with a buffer structure containing a
mutex, two condition variables, and the buffer's data
and indices. The
insertandremovefunctions acquire the mutex on entry, wait on the appropriate condition variable, and release the mutex on exit.
Create producer and consumer threads that use these functions, and verify that the items arrive in order. Compile withtypedef struct { pthread_mutex_t mutex; pthread_cond_t not_full; pthread_cond_t not_empty; int buffer[N]; int in, out, count; } buffer_t; void buffer_init(buffer_t *b) { pthread_mutex_init(&b->mutex, NULL); pthread_cond_init(&b->not_full, NULL); pthread_cond_init(&b->not_empty, NULL); b->in = b->out = b->count = 0; } void buffer_insert(buffer_t *b, int item) { pthread_mutex_lock(&b->mutex); while (b->count == N) pthread_cond_wait(&b->not_full, &b->mutex); b->buffer[b->in] = item; b->in = (b->in + 1) % N; b->count++; pthread_cond_signal(&b->not_empty); pthread_mutex_unlock(&b->mutex); } int buffer_remove(buffer_t *b) { pthread_mutex_lock(&b->mutex); while (b->count == 0) pthread_cond_wait(&b->not_empty, &b->mutex); int item = b->buffer[b->out]; b->out = (b->out + 1) % N; b->count--; pthread_cond_signal(&b->not_full); pthread_mutex_unlock(&b->mutex); return item; }gcc -O2 -pthread -o pc pc.c. -
Break the while loop.
Change the
whileloops inbuffer_insertandbuffer_removetoifstatements. Run the program with several producers and consumers. Do the errors appear? Run it many times and note whether the failure is consistent or intermittent. Restore thewhileloops. -
Replace signal with broadcast.
Change
pthread_cond_signaltopthread_cond_broadcastin both functions. Run the program. Does it still work? Time it and compare to the signal version. Does the broadcast slow it down? By how much? Explain the difference. -
Implement the same solution in Java.
Write a Java class with a shared buffer, a
synchronizedinsertmethod, asynchronizedremovemethod, and calls towait(),notify(), andnotifyAll().
Compile withclass BoundedBuffer { private final int[] buffer = new int[N]; private int in = 0, out = 0, count = 0; public synchronized void insert(int item) throws InterruptedException { while (count == N) wait(); buffer[in] = item; in = (in + 1) % N; count++; notifyAll(); } public synchronized int remove() throws InterruptedException { while (count == 0) wait(); int item = buffer[out]; out = (out + 1) % N; count--; notifyAll(); return item; } }javacand run. Verify that the program works and that the items arrive in order. - Compare the two implementations. Write a short table comparing the C and Java versions: how many lines of synchronization code does each require? What does the programmer have to remember in each? What errors can the compiler catch in each? Which is easier to get right? Which is easier to read?
-
Demonstrate the Java monitor.
Use the
javaptool to disassemble a class with asynchronizedmethod and observe themonitorenterandmonitorexitbytecode instructions that the compiler inserted.
These instructions are the JVM's implementation of the monitor's mutual exclusion. The programmer never writes them; the compiler inserts them at everyjavac MyClass.java javap -c MyClasssynchronizedboundary. This is the structural guarantee that distinguishes a monitor from a lock. -
Implement the dining philosophers with a monitor in C.
Adapt the monitor pattern to the dining philosophers
problem. Use a single mutex for the table and one
condition variable per philosopher. Verify that all
five philosophers eventually eat and that no two
adjacent philosophers eat simultaneously.
Run for 30 seconds with each philosopher eating and thinking repeatedly. Count the meals per philosopher and verify that the counts are roughly equal. If one philosopher starves, examine the order of thetypedef struct { pthread_mutex_t mutex; pthread_cond_t self[5]; int state[5]; /* 0=THINKING, 1=HUNGRY, 2=EATING */ } table_t;testcalls inputdown. - Observe the cost of a condition variable. Write a small benchmark that acquires a mutex, waits on a condition variable, and is signalled by another thread. Measure the round-trip time for one million such handoffs. Compare to the spinlock handoff measured in Lesson 6.2. How much does the monitor pattern cost compared to a raw lock handoff?
Reflect: the Java version of the
producer–consumer requires no explicit lock or unlock,
because the synchronized keyword makes the
monitor's mutual exclusion automatic. The C version
requires the programmer to lock and unlock the mutex on
every path. What does this tell you about the difference
between a language-level abstraction and a library
abstraction? Would you use a monitor-style pattern in C
for a production system, or would you prefer to use
something else? What would you need to make the C
version as safe as the Java version?
07 Knowledge check
Check your understanding
Question 1
Define a monitor. What three things does it package together, and what is enforced automatically by the language? How does this differ from a lock or a semaphore?
Show answer
Question 2
What is a condition variable? What are its three
operations, and how does it differ from a semaphore?
Why must wait always be called inside a
while loop?
Show answer
wait, which releases the monitor,
suspends the calling thread, and adds it to the
condition variable's wait queue;
signal, which wakes one thread waiting
on the condition variable if any; and
broadcast, which wakes all of them. A
condition variable differs from a semaphore in the
most important respect: it has no
memory. A semaphore's post
increments a value, and a later wait
finds the value and does not block. A condition
variable's signal has no effect if no
thread is waiting — it is lost. This is because the
condition variable is always used inside a monitor,
where the condition it represents is a property of
the shared data. If the condition is true, a thread
that later enters the monitor will see it directly,
without needing a remembered signal. The signal is a
wakeup, not a value. The wait must
always be called inside a while loop
because of Mesa semantics: when a
waiting thread is woken, other threads may have
entered the monitor in the meantime and changed the
shared data. The condition the thread was waiting for
may no longer hold. Re-checking the condition after
the wait ensures that the thread proceeds only when
the condition is actually true. There is also the
possibility of spurious wakeups, where the runtime
wakes a thread without a signal; the loop handles
those as well. A wait inside an
if is correct only under Hoare
semantics, which real implementations do not provide.
Question 3
Compare Hoare semantics (signal-and-wait) with Mesa semantics (signal-and-continue). What are the consequences of each for the programmer, and which is more common in practice?
Show answer
signal, it immediately gives up
the monitor to the woken thread, which runs as soon
as the signaller suspends. The signaller is placed in
a special queue and resumes when the woken thread
leaves the monitor or waits again. The woken thread
can be sure that the condition it was waiting for is
still true when it wakes, because no other thread has
had a chance to change it. Under Mesa
semantics, the signalling thread continues
executing, and the woken thread is moved to the
monitor entry queue, where it must compete with new
arrivals for the monitor. By the time the woken
thread actually runs, other threads may have entered
the monitor and changed the shared data, so the
condition may no longer hold. The consequence for the
programmer is that under Hoare semantics an
if suffices to test the condition before
waiting, while under Mesa semantics a
while loop is required — the condition
must be re-checked after every wakeup. Since Mesa
semantics is what nearly every real implementation
provides (Java, C#, the POSIX condition variable, and
the condition variables in most other libraries),
the while form is the correct one to
write in practice. Hoare semantics is pedagogically
important for understanding the concept, and it is
the semantics Hoare described in his original paper,
but it is harder to implement and leads to more
context switches, because every signal that wakes a
thread causes the signaller to be suspended and
resumed.
Question 4
Write the monitor-based producer–consumer solution. Explain how it differs from the semaphore-based solution of Lesson 6.4, and identify the advantages and disadvantages of each.
Show answer
BoundedBuffer with the buffer's data,
the indices, a count, and two condition variables
not_full and not_empty. The
insert procedure waits while
count == N, inserts the item, increments
the count, and signals not_empty. The
remove procedure waits while
count == 0, removes the item, decrements
the count, and signals not_full. The
producer calls insert; the consumer
calls remove. The differences from the
semaphore solution: (1) The monitor solution has
no explicit locking. The programmer
writes no acquire, no release, no wait on a mutex,
no post on a mutex. The mutual exclusion is provided
automatically by the runtime. (2) The monitor
solution uses condition variables,
which are tied to specific conditions
(count == N and count == 0)
rather than to counts of resources. The condition
is expressed directly in the code, rather than being
encoded in the initial value of a semaphore. (3)
The monitor solution has no fixed order of
acquisition. There is no way to write the
operations in an order that deadlocks, because the
monitor is acquired automatically and the conditions
are checked before waiting. The semaphore solution
required the programmer to get the order of the two
waits right, and reversing them deadlocked. The
advantages of the monitor solution are readability
and safety: it is shorter, clearer, and harder to get
wrong. The disadvantage is that it requires language
support. C and POSIX do not provide monitors; the
closest equivalent is the mutex-plus-condition-variable
pattern, which requires the programmer to write the
lock and unlock explicitly and therefore loses the
structural guarantee. In a language that provides
monitors (Java, C#), the monitor solution is
preferable; in C, the semaphore solution is often
more convenient because it is fully expressed in the
library rather than relying on a language feature
that does not exist.
Question 5
What is the difference between signal and
broadcast on a condition variable? When
should each be used? What is the cost of always using
broadcast?
Show answer
signal wakes exactly
one waiter on the condition variable; if no thread
is waiting, the signal is lost.
broadcast wakes all
waiters; each of them must re-check its condition and
go back to sleep if the condition is still false.
signal is appropriate when the condition
that has become true allows exactly one waiter to
proceed. In the producer–consumer solution, one
inserted item allows exactly one consumer to remove
it, so a signal on not_empty
is correct and more efficient than a
broadcast. broadcast is
appropriate when the condition that has become true
may allow more than one waiter to proceed, or when
the waiters are heterogeneous — waiting on different
aspects of the same condition — and the signaller
cannot know which one can proceed. The dining
philosophers problem is an example: when a
philosopher puts down their forks, both neighbours may
be able to eat, and the signaller does not know which
of them will succeed. A broadcast wakes
both, and each re-checks its own condition. The cost
of always using broadcast is that it
wakes threads that will immediately go back to sleep.
Each of those wakeups causes a context switch and
contention on the monitor. On a system with many
waiters, the wasted wakeups can be a substantial
fraction of the total CPU time. The cost is
acceptable when the number of waiters is small or
when the wakeup rate is low; it is significant when
the number of waiters is large or the wakeup rate is
high. The general rule is to use signal
when the condition is specific and to reserve
broadcast for the cases where the
condition is general and more than one waiter may
proceed.
08 Unit discussion
Discussion prompt
The monitor was a response to the difficulty of using semaphores correctly. Its key insight was that synchronization should be enforced by the language, not by the programmer's discipline: if the compiler can see that a region of code is mutually exclusive, it can insert the lock and unlock automatically, and a whole class of errors becomes impossible by construction.
Forty years later, the same idea is still being reinvented
in new forms. Rust enforces data-race freedom at compile
time through ownership and borrowing. Go encourages
communication over shared memory and provides
goroutines and channels. Erlang and Elixir use actor
models with no shared state at all. Swift has
actor types. Each of these is an attempt to
move the synchronization discipline from the programmer
into the language, and each has a different theory of
what kind of error is most worth preventing.
What is the right abstraction? Should the language enforce mutual exclusion automatically, at the cost of some performance and some flexibility? Should it enforce stronger properties — no data races at all, as Rust does — at the cost of a more complex type system? Or should it step back entirely, as Erlang does, and give the programmer a fundamentally different model of concurrency in which shared state does not arise? Consider the languages you know: which one has the best concurrency story, and why?
Post your response to the Unit 6 discussion forum and reply to at least two classmates. Supports CLO2, CLO3, and CLO8.
09 Practice exercise
This ungraded exercise consolidates the lesson and prepares you for Lesson 6.6. Aim to spend 90–120 minutes on it.
-
Trace the monitor producer–consumer.
Using the monitor code from section 4.4 with N = 3,
trace the following sequence of operations under Mesa
semantics: C1.remove(), C2.remove(), P.insert(1),
P.insert(2), P.insert(3), C1.run, P.insert(4). Show
the value of
countand the contents of thenot_fullandnot_emptyqueues after each operation. At which points does a thread block? At which points is a thread woken? -
Write the producer–consumer in Java.
Write a Java program with a single producer and a
single consumer sharing a bounded buffer. Use the
monitor pattern with
synchronizedmethods andwait/notifyAll. Test with a buffer of 5 slots and 100,000 items, and verify that the items arrive in order. Then changenotifyAlltonotifyand run again. Does the program still work? Why or why not? -
Write the producer–consumer in C.
Write the same program in C using the monitor pattern
with
pthread_mutex_tandpthread_cond_t. Compile with-O2 -pthread. Test with a buffer of 5 slots and 100,000 items. Compare the number of lines of synchronization code to the Java version. -
Break the while loop.
In the C version, change the
whileloops toifstatements. Run with three producers and three consumers. Run the program ten times and record how many runs produce an error. Then restore thewhileloops and run again. Does the error disappear entirely? Why is it intermittent? -
Compare signal and broadcast.
Modify the C program to use
pthread_cond_broadcastinstead ofpthread_cond_signal. Run with five producers and five consumers on a small buffer. Time the program for 100,000 items with signal and with broadcast. Which is faster? By how much? Explain the difference in terms of the number of useless wakeups. - Implement the dining philosophers with a monitor. Write the dining philosophers monitor from section 4.9 in C using the mutex-and-condition-variable pattern. Run for 30 seconds with five philosophers, each alternating between eating and thinking for random durations. Count the number of meals per philosopher. Are the counts roughly equal? Which philosopher eats most, and why?
-
Examine Java bytecode for synchronized.
Compile a Java class with a
synchronizedmethod and disassemble it withjavap -c. Identify themonitorenterandmonitorexitinstructions that the compiler inserted. Write a short paragraph explaining what these instructions do and why they are the implementation of the monitor's mutual exclusion. - Reflect. In your own words, explain what it means to say that a monitor provides mutual exclusion as a structural guarantee rather than a conventional one. Give one example of an error that is possible with a mutex and impossible with a monitor, and one advantage that a mutex has over a monitor in languages that provide both.
10 Summary
- A monitor is a language-level construct that packages shared data, the procedures that operate on it, and the synchronization that protects it into a single unit. It was introduced by Brinch Hansen (1973) and Hoare (1974).
- The essential property of a monitor is that at most one thread is executing a monitor procedure at a time. The runtime enforces this automatically; the programmer writes no lock or unlock.
- This structural guarantee distinguishes the monitor from the lock and the semaphore, where the synchronization is the programmer's responsibility and the compiler cannot check it.
-
A condition variable allows a thread to
wait for a condition inside a monitor. Its operations are
wait(release the monitor and suspend),signal(wake one waiter), andbroadcast(wake all waiters). - A condition variable has no memory: a signal that arrives when no thread is waiting has no effect. This is different from a semaphore, whose post increments a value that a later wait can find.
- Mesa semantics (signal-and-continue) is what nearly every real implementation provides. Under Mesa semantics, the signaller continues and the woken thread competes for the monitor, so the condition may no longer hold when the woken thread runs.
- Hoare semantics (signal-and-wait) is the semantics Hoare described in his original paper: the signaller immediately yields the monitor to the woken thread. It is simpler to reason about but harder to implement and causes more context switches.
-
The consequence of Mesa semantics is that every
waitmust be inside awhileloop that re-checks the condition. Awaitinside anifis correct only under Hoare semantics and is a common bug. - The monitor solution to the bounded-buffer producer–consumer problem uses one condition variable for "buffer not full" and one for "buffer not empty". It is shorter and clearer than the semaphore solution, and it cannot deadlock from a reversed acquisition order.
-
Modern languages implement monitors in
various forms: Java's
synchronizedmethods andwait/notify, C#'slockstatement andMonitorclass, and the condition variable libraries of Python and Ruby. C and POSIX do not provide monitors directly; the closest equivalent is the mutex-plus-condition-variable pattern, which requires the programmer to write the synchronization explicitly.
Looking ahead
This lesson introduced the condition variable as the
monitor's mechanism for waiting on a condition. Lesson
6.6 examines the condition variable in detail as a
primitive in its own right. It presents the POSIX
condition variable
(pthread_cond_t), its use together with a
mutex, the reasons for the loop-around-wait pattern, the
difference between signal and
broadcast with concrete examples, and the
classic applications — the producer–consumer problem,
the reader–writer problem, and the barrier. The
condition variable is the most subtle primitive in the
unit, and it is the one most often misused. Lesson 6.6
will give you the tools to use it correctly.
11 References
- Silberschatz, A., Galvin, P. B., & Gagne, G. Operating System Concepts, 10th ed., Chapter 6, Section 6.7.
- Tanenbaum, A. S., & Bos, H. Modern Operating Systems, 4th ed., Chapter 2, Section 2.3.7.
-
Arpaci-Dusseau, R. H., & Arpaci-Dusseau, A. C.
Operating Systems: Three Easy Pieces, Chapter 30.
Available online at
ostep.org. - Hoare, C. A. R. Monitors: An Operating System Structuring Concept. Communications of the ACM, 1974. The original paper on monitors and condition variables.
- Brinch Hansen, P. Operating System Principles. Prentice-Hall, 1973. The original presentation of the monitor concept.
- Lampson, B. W., & Redell, D. D. Experience with Processes and Monitors in Mesa. Communications of the ACM, 1980. The paper that introduced signal-and-continue semantics.
- Lea, D. Concurrent Programming in Java: Design Principles and Patterns, 2nd ed. Addison-Wesley, 1999. The Java memory model and the implementation of monitors in the JVM.
-
Linux man-pages:
man 3 pthread_cond_init,man 3 pthread_cond_wait,man 3 pthread_cond_signal,man 3 pthread_cond_broadcast,man 3 pthread_cond_destroy.