Operating System Slides 📂 Introduction · 11 of 22 40 min read

Process Synchronization — Critical Section, Peterson's, Semaphores & Monitors

Master process synchronization in operating systems: race conditions, the critical-section problem and its three requirements, Peterson's algorithm, atomic hardware instructions, counting and binary semaphores, the producer–consumer problem, and monitors — with animated diagrams throughout.

🔐

Process Synchronization — Critical Section, Peterson's, Semaphores & Monitors

When many processes touch the same data, timing becomes a bug. Learn the critical-section problem and the tools that solve it — Peterson's algorithm, atomic hardware, semaphores and monitors — with animated diagrams.
Race Conditions Critical Section Semaphores Monitors

Press Next → or use ← → arrow keys

Section 01

The Story That Explains Synchronization

Anita and Rohan share an account with ₹1000. At the same instant, from two different ATMs, Anita withdraws ₹700 and Rohan withdraws ₹500. Both machines read the balance (₹1000) first, each subtracts its amount, and each writes back. One write clobbers the other — the bank loses money and the final balance is simply wrong.

The withdrawal should have been one indivisible step. Because it wasn't, the result depends on who wrote last — a race condition.
🎲
Race Condition

A race condition occurs when the final state of shared data depends on the exact interleaving of concurrent operations. The outcome is non-deterministic — right sometimes, wrong other times — which makes these the hardest bugs to catch.

Section 02 · Diagram

Watch a Race Condition Happen

Producer · counter++ reg1 = counter (reads 5) reg1 = reg1 + 1 (= 6) counter = reg1 (writes 6) Consumer · counter-- reg2 = counter (reads 5) reg2 = reg2 − 1 (= 4) counter = reg2 (writes 4) shared counter 5 4 Producer's +1 was lost — should be 5, got 4
🔍
The Interleaving That Breaks It

counter++ and counter-- each compile to read, modify, write. If both read the old value (5) before either writes, the second write overwrites the first. One update simply vanishes. A single shared variable, three machine instructions, and the result is corrupt.

Section 03

The Critical-Section Problem

ENTRYrequest access 🔒 CRITICALshared data — 1 at a time EXITrelease access 🔓 REMAINDERother work 🔒
🚪
Every Concurrent Process Has This Shape

Code splits into four regions: an entry section that requests access, the critical section that touches shared data, an exit section that releases, and the remainder. The whole game is making the entry gate guarantee that only one process is in the red block at a time.

Section 03 · Requirements

Three Requirements of a Correct Solution

🔒
Mutual Exclusion
only one at a time
If a process is inside its critical section, no other process may enter its own critical section. This is the core safety property.
🚦
Progress
no useless waiting
If no one is in the critical section and some processes want in, the choice of who enters next can't be postponed indefinitely.
⚖️
Bounded Waiting
fairness guarantee
There's a limit on how many times other processes can jump ahead after a process requests entry — so nobody starves.
⚠️
All Three, Or It's Wrong

A solution that gives mutual exclusion but lets a process wait forever fails bounded waiting. One that's fair but occasionally lets two processes in fails mutual exclusion. A correct answer satisfies all three at once.

Section 04

A First Attempt — And Why It Fails

// Strict alternation with a shared turn variable
int turn = 0;   // whose turn is it (0 or 1)

// Process P0                    // Process P1
while (turn != 0) ;         while (turn != 1) ;
// CRITICAL SECTION            // CRITICAL SECTION
turn = 1;                     turn = 0;
🚫
It Gives Mutual Exclusion but Breaks Progress

Strict alternation forces P0, P1, P0, P1… forever. If P0 finishes and doesn't want to enter again, P1 still can't proceed until P0 takes another turn — which never comes. A process is blocked by another that isn't even interested. That violates progress.

Section 05

Peterson's Solution — The Two-Process Answer

Process P0 flag[0]=true; turn=1; ★ IN CRITICAL SECTION shared state flag[0]=T flag[1]=T turn = 0 Process P1 flag[1]=true; turn=0; ⏸ WAITING… ✅ mutual exclusion preserved — only P0 is inside
// Pi wants in;  j = 1 − i
flag[i] = true;               // (1) I want to enter
turn    = j;                  // (2) but I let YOU go first
while (flag[j] && turn == j);  // (3) wait if other wants in AND it's their turn
// ... CRITICAL SECTION ...
flag[i] = false;              // (4) done — you can go
🤝
The "After You" Trick

Each process raises its own flag to signal intent, then politely sets turn to the other. Whoever set turn last yields — so exactly one proceeds. Peterson's satisfies all three requirements for two processes, using only shared memory, no special hardware.

Section 06

Hardware Support — Atomic Instructions

Software locks are fiddly. Modern CPUs provide atomic read-modify-write instructions that can't be interrupted halfway — the hardware foundation every lock is built on.

🔬
Test-and-Set
read + set, atomically
Returns a lock's old value and sets it true in one indivisible step. Spin until it returns false and you've grabbed the lock.
🔄
Compare-and-Swap
CAS
Writes a new value only if the current value matches what you expected — the basis of lock-free data structures.
🌀
Spinlock
busy-waiting
A loop on test-and-set. Cheap when the wait is tiny, wasteful on a single CPU — it burns cycles doing nothing.
// Spinlock built on Test-and-Set
do {
    while (TestAndSet(&lock)) ;   // spin until we get the lock
    // ... CRITICAL SECTION ...
    lock = false;                  // release
} while (true);
Section 07

Semaphores — Dijkstra's Elegant Answer

A semaphore is just an integer touched only through two atomic operations — wait() (P) and signal() (V).

wait(S) {                signal(S) {
    while (S <= 0) ;           S = S + 1;
    S = S − 1;             }
}
0️⃣
Binary Semaphore
value ∈ {0,1}
Behaves like a mutex — one key, one holder. Perfect for guarding a single critical section.
🔢
Counting Semaphore
value ∈ {0…N}
Tracks N interchangeable resources — e.g. 5 database connections. Each wait() takes one, each signal() returns one.
😴
Blocking vs Spinning
sleep, don't spin
A good implementation puts a blocked process on a queue and sleeps it, then wakes one on signal() — no wasted CPU.
Section 07 · Diagram

Binary Semaphore in Action

CRITICAL SECTION PA inside 🔒 only one occupant mutex semaphore 1 0 WAITING QUEUE PA PB PA: wait(mutex) → 1→0, enters · PB: wait(mutex) → blocks · PA: signal → PB wakes
🔑
One Key, Passed Hand to Hand

PA calls wait(mutex): the value drops 1→0 and PA enters. PB calls wait(mutex) but the value is 0, so it blocks in the queue. When PA calls signal(mutex), PB is woken and takes the key. Mutual exclusion, guaranteed.

Section 08

Producer–Consumer With Three Semaphores

PRODUCERgenerating… CONSUMERreading… A B C D bounded buffer · size 4 empty full mutex
semaphore mutex = 1, empty = N, full = 0;
// PRODUCER            // CONSUMER
wait(empty);           wait(full);
wait(mutex);           wait(mutex);
// add item          // remove item
signal(mutex);         signal(mutex);
signal(full);          signal(empty);
🎛️
Three Semaphores, Three Jobs

mutex guards the buffer (mutual exclusion); empty counts free slots so the producer blocks when full; full counts items so the consumer blocks when empty. Order matters — always take the counting semaphore before the mutex, or you can deadlock.

Section 09

Semaphore Pitfalls

🔁
Forgotten Signal
Miss a signal() and the resource is never released — every waiter blocks forever.
🔀
Wrong Order
Two processes grabbing two semaphores in opposite orders → classic deadlock.
↩️
Swapped Calls
signal() before wait(), or double-waiting, silently breaks mutual exclusion.
🧨
Powerful, but Unforgiving

Semaphores are correct only if every programmer uses them perfectly, everywhere. One misplaced call anywhere in a large codebase can corrupt data or freeze the system — and the bug may appear only once in a million runs. That fragility is exactly what monitors were invented to fix.

Section 10

Monitors — Language-Level Synchronization

ENTRY QUEUE P1 P2 P3 waiting to enter ACTIVE IN MONITOR 🧠 only ONE at a time CONDITION VAR notEmpty.wait() sleeping on a predicate
🧱
Mutual Exclusion, Automatically

A monitor bundles shared data with the procedures that touch it, and the compiler guarantees only one process is active inside at a time — you never call wait/signal by hand. Inside, a condition variable lets a process sleep until a predicate holds (notEmpty.wait()) and be woken by another (notEmpty.signal()). Java's synchronized is a monitor.

Section 12

Semaphore vs Monitor

FeatureSemaphoreMonitor
LevelLow-level OS primitiveHigh-level language construct
Mutual exclusionManual — call wait/signalAutomatic — enforced by compiler
Signal semanticsRemembered (increments the count)Lost if no one is waiting
Programmer effortHigh — easy to bugLow — structured
Error-proneYes (deadlock, forgot signal)Much less so
SupportOS syscall / libraryBuilt-in (Java synchronized, C# lock)
Best forKernel code, resource countingApplication code, structured concurrency
⚠️
One Crucial Difference

A semaphore's signal() is remembered — signal now, a later wait() still succeeds. A condition variable's signal() is stateless — if nobody is waiting, it's lost. That's why monitor code always re-checks the condition in a while loop, never an if.

Section 14

Eight Rules for Synchronization

🔐 PROCESS SYNCHRONIZATION · CHECKLIST
1
Protect every access to shared mutable data — one unguarded write is enough to corrupt state.
2
A correct solution satisfies all three: mutual exclusion, progress, bounded waiting.
3
Peterson's algorithm solves it for two processes in software alone.
4
Atomic hardware (test-and-set, compare-and-swap) is the foundation under every real lock.
5
A semaphore's signal is remembered; a condition variable's is stateless — re-check in a while.
6
Always acquire multiple locks in a consistent order to avoid deadlock.
7
Keep critical sections as short as possible — hold the lock only while touching shared data.
8
Prefer monitors for application code; reserve raw semaphores for the kernel.
FINAL

From Race Conditions to Reliable Concurrency

3Requirements
2Processes Peterson solves
P·Vwait / signal
3Semaphores for bounded buffer
1Process active in a monitor
🎯
You Now Understand the Whole Toolkit

From the race condition that starts it all, through the critical-section requirements, Peterson's software solution, atomic hardware, semaphores and monitors — you can reason about any concurrency problem and pick the right tool to keep shared data safe.

📚
Where To Go Next

Apply these tools to the classic problems — readers–writers and dining philosophers — then study deadlock: how it arises and how to prevent, avoid, or detect it.

🔐 End of tutorial · Press to review, or click Restart