Interleaving Threads: Synchronization Through Monitors
April 17, 2026 · Luciano Muratore
Interleaving Threads: Synchronization Through Monitors
In multithreading, we often want two threads to take turns, not just run concurrently, but alternate in a strict, predictable order.
A classic example of this is: let t1 print odd numbers and t2 print even numbers, so that the final output is 1, 2, 3, 4, 5, ... up to 100. At first glance, this sounds simple. But threads do not naturally respect each other’s timing. Without synchronization, both threads would race, and the output would be chaotic.
So the question is: how do we make two threads interleave each other, in order?
The Building Blocks
The solution relies on three shared primitives:
- A shared boolean
turn, to know whose turn it is. - A mutex, to protect access to
turn. - A condition variable, to put a thread to sleep and wake the other one up.
Together, these three form what is known as a Monitor—a synchronization construct that bundles a mutex and a condition variable into a single, coherent unit.
The Monitor
class OddEvenMonitor {
public:
bool turn = true; // true = odd thread's turn, false = even thread's turn
mutex mx;
condition_variable cv;
void WaitTurn(bool myTurn) {
unique_lock<mutex> lock(mx);
// If it's not my turn, sleep until notified
while (turn != myTurn) {
cv.wait(lock);
}
}
void ToggleTurn() {
lock_guard<mutex> lock(mx);
turn = !turn; // Switch whose turn it is
cv.notify_one(); // Wake the waiting thread
}
};
The design here is deliberate. WaitTurn acquires the lock and checks the condition in a while loop,not an if. This protects against spurious wakeups, a known behavior of condition variables where a thread can be woken up without being explicitly notified. By looping, we ensure the thread only proceeds when turn truly matches.
ToggleTurn acquires the lock, flips turn, and calls notify_one to wake whichever thread is waiting. It uses lock_guard because there is no need to release early—the operation is short and unconditional.
The Threads
With the monitor in place, the threads become simple:
void OddThread(Monitor& m) {
for (int number = 1; number <= 99; number += 2) {
// Wait until the monitor says:
// "It's the odd thread's turn."
m.WaitTurn(ODD_TURN);
// Now it's safe to print
cout << number << "\n";
// Pass control to the even thread
m.ToggleTurn();
}
}
The even thread follows the same pattern, but starts at 2, increments by 2, and waits for EVEN_TURN.
The sequence becomes:
- Odd waits for its turn → prints
1→ toggles turn. - Even wakes up → prints
2→ toggles turn. - Odd wakes up → prints
3→ toggles turn. - Repeat until
100.
Why This Works
The key insight is that at any given moment, exactly one thread is running and the other is sleeping. There is no race to print, no overlap, no out-of-order output.
The condition variable is what makes this efficient. Instead of spinning in a busy loop checking turn over and over, the waiting thread sleeps, it releases the CPU entirely, and only wakes when signaled. This is the difference between polling and notification-based synchronization.
The mutex ensures that reading and writing turn is always atomic. Without it, both threads could read the old value of turn simultaneously, and the guarantee would break.
Summary
- Two threads can interleave in strict order using a shared boolean, a mutex, and a condition variable.
- Wrapping these into a Monitor keeps the synchronization logic clean and encapsulated.
- The
whileloop inWaitTurnguards against spurious wakeups. notify_onewakes exactly one waiting thread—no thundering herd, no wasted wakeups.- Final Insight: Ordered interleaving is not magic. It is just a thread patiently waiting for its turn, and another thread that always remembers to pass the baton.