Operating Systems 📂 Process Synchronization · 1 of 1 64 min read

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

Master Process Synchronization from Galvin's Operating System Concepts. This tutorial explains race conditions with an animated example, formalises the critical-section problem and its three requirements, walks step-by-step through Peterson's two-process solution, introduces semaphores with visual wait/signal semantics, solves the bounded buffer problem, and covers monitors and condition variables. Includes hardware atomics, Java's synchronized keyword, and a comparison table.

Section 01

The Story That Explains Process Synchronization

The Shared Bank Account Disaster
Anita and Rohan share a bank account with ₹1000 balance. On Monday morning, both walk into different ATMs at exactly the same second. Anita withdraws ₹700; Rohan withdraws ₹500. A correct system would allow only one — the second would fail for insufficient funds.

But suppose the ATMs execute in this order:
  • Anita's ATM reads balance = ₹1000
  • Rohan's ATM reads balance = ₹1000 (before Anita's update lands)
  • Anita's ATM writes new balance = 1000 − 700 = ₹300
  • Rohan's ATM writes new balance = 1000 − 500 = ₹500

Both got their cash. Final balance: ₹500. The bank just lost ₹200 — because two operations that should have been atomic got interleaved.

This is a race condition, and preventing it is exactly what Process Synchronization is about.
💡
The Core Problem

When multiple processes access shared data concurrently, the final result depends on the order of execution. If nothing enforces ordering, the outcome is unpredictable and often wrong. Synchronization is the discipline of controlling access to shared resources so that concurrent execution still produces correct results.


Section 02

Background — Why Concurrency Breaks Things

In a modern OS, multiple processes and threads run concurrently. On a uniprocessor, the scheduler rapidly switches between them, giving the illusion of parallelism. On a multicore machine, they may truly execute at the same instant. Both scenarios cause the same problem: the outcome depends on interleaving.

Consider a shared variable counter = 5. Producer wants counter++. Consumer wants counter--. Each compiles into three machine instructions:

📈 Producer: counter++
register1 = counter
register1 = register1 + 1
counter = register1
📉 Consumer: counter--
register2 = counter
register2 = register2 − 1
counter = register2

🎮 Interactive — Watch the Race Unfold, Step by Step

Race Condition — click Next to advance one instruction at a time
PRODUCER (counter++) register1 CONSUMER (counter--) register2 reg1 = ? reg2 = ? SHARED counter 5 reg1 = counter reg1 = reg1 + 1 counter = reg1 reg2 = counter reg2 = reg2 − 1 counter = reg2 ❌ WRONG! Producer's +1 was lost. Should be 5, got 4.
Step 0 of 6
START Initial state: shared counter = 5. Both processes want to modify it. Click Next to see one machine instruction at a time.
⚠️
Definition — 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: sometimes correct, sometimes not. Race conditions are the number-one source of concurrency bugs in production systems.


Section 03

The Critical-Section Problem

Galvin formalises the problem as follows. Each of n processes has a segment of code called a critical section, in which it accesses shared variables, modifies a common file, or updates shared data structures. When one process is executing in its critical section, no other process may execute in their critical section.

The Four Regions of Every Process
Structure of Every Concurrent Process ENTRY request access CRITICAL shared data! EXIT release access REMAINDER other code The blue "entry" gate must guarantee only ONE process is in the red block at any time.

The Three Requirements

🔒
1. Mutual Exclusion
only one at a time
If process Pi is executing in its critical section, then no other process can be executing in their critical section. The fundamental safety property.
2. Progress
no useless waiting
If no process is in the critical section, and some processes wish to enter, then the selection of which enters next cannot be postponed indefinitely.
3. Bounded Waiting
fairness guarantee
There must exist a bound on the number of times other processes can enter their critical sections after a process Pi has made a request.

Section 04

A First Attempt — And Why It Fails

Naive students often reach for a simple boolean lock. Let's see why every naive attempt breaks at least one of the three requirements.

Attempt 1 — Strict Alternation using turn

// Shared variable
int turn = 0;   // whose turn is it (0 or 1)

// Process P0                        Process P1
while (true) {                     while (true) {
    while (turn != 0);  // wait       while (turn != 1);
    // CRITICAL SECTION              // CRITICAL SECTION
    turn = 1;                        turn = 0;
    // remainder                     // remainder
}                                    }
Why It Fails — Progress Violation

Mutual exclusion holds, but progress fails. If P0 finishes its critical section and enters a long remainder, P1 can enter only once. If P1 wants to enter again, it must wait for P0 — even if P0 doesn't want in. Strict alternation forces useless waiting.


Section 05

Peterson's Solution — The Two-Process Answer

In 1981, Gary Peterson published an elegant software-only solution for two processes. It combines a shared turn variable and a shared flag[] array to signal intent. Peterson's algorithm satisfies all three requirements.

// Shared variables
int  turn;              // whose turn is it
boolean flag[2];        // flag[i] = true means Pi wants in

// Process Pi (i = 0 or 1, j = 1 − i)
while (true) {
    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 AND it's their turn

    // CRITICAL SECTION

    flag[i] = false;                       // (4) I'm done, you can go
    // remainder section
}

🎮 Interactive — Walk Through Peterson's Algorithm

Peterson's Algorithm — both processes want CS at the same time
Process P0 (i=0, j=1) Process P1 (i=1, j=0) SHARED STATE flag[0]: false flag[1]: false turn: in critical section: none 1: flag[0] = true 2: turn = 1 3: while(flag[1] && turn==1) 4: ★ CRITICAL SECTION ★ 5: flag[0] = false 1: flag[1] = true 2: turn = 0 3: while(flag[0] && turn==0) 4: ★ CRITICAL SECTION ★ 5: flag[1] = false ⏸ WAITING… ⏸ WAITING… ✅ Mutual exclusion preserved throughout
Step 0 of 8
START Both P0 and P1 want to enter the critical section at nearly the same time. Watch how the shared turn variable resolves the tie.
🔑
Why Peterson's Solution Works

The trick is setting turn = j after raising your flag. If both processes race and both set turn, only the last write survives. The process whose write happened last effectively said "you go first" — and it waits. The other one proceeds. Elegant, minimal, and mathematically provable.


Section 06

Hardware Support — Atomic Instructions

Modern hardware provides atomic instructions that make locking easy. Two are especially famous.

🔧 TestAndSet(target)
boolean TestAndSet(boolean *target) {
  boolean rv = *target;
  *target = true;
  return rv;
}
🔧 CompareAndSwap(v, exp, new)
int CompareAndSwap(int *v,
  int expected, int new_val) {
  int temp = *v;
  if (temp == expected) *v = new_val;
  return temp;

The key property: these instructions execute indivisibly — the CPU guarantees no other core can interleave with them. This is enough to build a spinlock:

// Spinlock using TestAndSet
do {
    while (TestAndSet(&lock)) ;   // spin until we get the lock
    // CRITICAL SECTION
    lock = false;                  // release
    // remainder section
} while (true);
⚠️
The Spinlock Problem

A spinlock busy-waits, burning CPU cycles. On a uniprocessor this is pure waste. Spinlocks are only reasonable inside kernels on multiprocessors, where the wait is short. For general use, we need semaphores.


Section 07

Semaphores — Dijkstra's Elegant Answer

In 1965, Edsger Dijkstra introduced the semaphore — an integer accessed only through two atomic operations: wait() (P) and signal() (V).

// The two atomic operations
wait(S) {                   signal(S) {
    while (S <= 0) ;              S = S + 1;
    S = S − 1;                }
}
🔓
Binary Semaphore (Mutex)
value ∈ {0, 1}
Value is either 0 (locked) or 1 (available). Equivalent to a mutex lock. Used for pure mutual exclusion.
🔢
Counting Semaphore
value ∈ {0, 1, …, N}
Value is any non-negative integer. Controls access to a resource with a finite number of instances — e.g. N printers.
😴
Blocking Semaphores
no busy-wait
Real implementations don't spin. When wait() would block, the process is moved to the semaphore's waiting queue and put to sleep.

🎮 Interactive — Semaphore Wait/Signal Sequence

Binary Semaphore in Action — two processes competing for one CS
CRITICAL SECTION only one occupant — empty — MUTEX SEMAPHORE value = 1 WAITING QUEUE (empty) PA Process A ready PB Process B ready CURRENT ACTION PA: wait(mutex)
Step 0 of 6
START Mutex initialised to 1 (available). Both processes want the critical section. Click Next to trace wait() and signal() calls one at a time.
semaphore mutex = 1;   // initialised to 1 → binary semaphore

// Every process does:
while (true) {
    wait(mutex);        // decrement; block if 0
    // CRITICAL SECTION
    signal(mutex);      // increment; wake one waiter if any
    // remainder
}

Section 08

Bounded Buffer (Producer/Consumer) with Semaphores

A producer generates items and places them in a buffer of size N. A consumer removes them. We need three semaphores: mutex for the buffer, empty counting empty slots, full counting filled slots.

semaphore mutex = 1;         // mutual exclusion on buffer
semaphore empty = N;         // initially N empty slots
semaphore full  = 0;         // initially 0 filled slots

// PRODUCER                          // CONSUMER
while (true) {                     while (true) {
    // produce item                    wait(full);        // wait for item
    wait(empty);       // wait slot       wait(mutex);
    wait(mutex);                        // remove item
    // add item to buffer             signal(mutex);
    signal(mutex);                       signal(empty);    // slot free
    signal(full);      // item ready      // consume item
}                                    }

🎮 Interactive — Bounded Buffer of Size 4

Producer & Consumer sharing a 4-slot buffer
PRODUCER generating… CONSUMER reading… slot 0 slot 1 slot 2 slot 3 A B C D signal(full) wait(full) SEMAPHORE STATE mutex: 1 empty: 4 full: 0 Ready — click Next to begin
Step 0 of 8
START Buffer of size 4 begins empty. mutex=1, empty=4, full=0. Watch how the semaphores block the producer when full and block the consumer when empty.
🚩
Common Bug — Reversed Wait Order

If you do wait(mutex); wait(empty); in the producer, disaster: when the buffer is full, the producer blocks on empty while holding mutex. The consumer can never acquire mutex to remove an item. Deadlock. Rule: always acquire the counting semaphore first, then mutex.


Section 09

Semaphore Pitfalls

Deadlock
circular wait
Two processes each hold one semaphore and wait for the other. Neither can proceed.

P1: wait(A); wait(B);
P2: wait(B); wait(A);
Fix: always acquire in the same global order.
🔢
Starvation
indefinite blocking
If the semaphore's waiting queue uses LIFO ordering, an unlucky waiter may never wake. Real implementations use FIFO to guarantee bounded waiting.
🤯
Programmer Error
low-level primitive
Forgetting a signal(), calling wait() twice, or swapping wait/signal — all cause silent corruption. Semaphores are powerful but easy to misuse. This is exactly why monitors were invented.

Section 10

Monitors — Language-Level Synchronization

In 1974, Per Brinch Hansen and Tony Hoare proposed the monitor: a high-level construct that automatically enforces mutual exclusion. All shared data and all operations on it are encapsulated in one module; only one process can be active inside a monitor at a time.

🎮 Interactive — Monitor with Condition Variables

Monitor life cycle — entry queue, active process, condition variables
MONITOR ENTRY QUEUE (empty) waiting to enter ACTIVE IN MONITOR — none — only one at a time cond notEmpty (empty) waiting on condition SHARED DATA count = 0 P1 P2 P3 3 processes waiting outside
Step 0 of 7
START Three processes want to use a shared buffer through a monitor. The monitor enforces that only one process is active inside at any time. Click Next to see how entry queue and condition variable queue interact.
🔢 Condition Variable Operations
x.wait()
Suspends the caller, releases the monitor lock, joins x's waiting queue. Another process can now enter the monitor.
x.signal()
Wakes exactly one process waiting on x. If no one waits, the signal is lost (unlike a semaphore).
x.broadcast()
Wakes all processes waiting on x. Used when a change may satisfy multiple waiters.
⚠️
Semaphore vs Condition Variable — Critical Difference

A semaphore remembers signals — if you signal() before anyone waits, the count still increments. A condition variable is stateless: signal() on an empty queue is lost. This is why cond-var code always uses while (!condition) x.wait();.


Section 11

Monitor Example — Bounded Buffer

monitor BoundedBuffer {
    int     buffer[N];
    int     count = 0;
    condition notFull, notEmpty;

    procedure insert(int item) {
        while (count == N)
            notFull.wait();          // wait until room in buffer
        buffer[count] = item;
        count++;
        notEmpty.signal();          // wake a consumer
    }

    procedure remove() returns int {
        while (count == 0)
            notEmpty.wait();         // wait until an item exists
        int item = buffer[--count];
        notFull.signal();           // wake a producer
        return item;
    }

    initialization { count = 0; }
}

In Java — Every Object Is a Monitor

public class BoundedBuffer<T> {
    private final Object[] buf;
    private int count = 0;

    public BoundedBuffer(int capacity) { buf = new Object[capacity]; }

    public synchronized void put(T item) throws InterruptedException {
        while (count == buf.length) wait();
        buf[count++] = item;
        notifyAll();
    }

    public synchronized T take() throws InterruptedException {
        while (count == 0) wait();
        T item = (T) buf[--count];
        notifyAll();
        return item;
    }
}
🏆
Look What's Missing

No explicit mutex. No wait/signal on a counting semaphore. No danger of forgetting to release the lock. The monitor guarantees one operation runs at a time; the programmer only writes the logic. This is why every modern language uses monitor-style constructs.


Section 12

Comparison — Semaphore vs Monitor

FeatureSemaphoreMonitor
LevelLow-level OS primitiveHigh-level language construct
Mutex isManual — call wait/signalAutomatic — enforced by compiler
Waiting semanticsCounting or binaryCondition variables (stateless)
Signal semanticsAlways increments (remembered)Lost if no one waiting
Programmer effortHigh — easy to bugLow — structured
Error proneYes (deadlock, forgot signal)Less so
Language supportOS syscall / libraryBuilt-in (Java synchronized, C# lock)
Suitable forKernel code, resource countingApplication code, structured concurrency

Section 13

Real-World Applications

🖥️
Database Transactions
Every RDBMS uses lock managers to guarantee ACID isolation. Two transactions modifying the same row cannot execute concurrently.
MySQL, PostgreSQL, Oracle
📲
Network Servers
Nginx, Apache use counting semaphores to cap concurrent connections. A "worker pool" of size N is exactly a counting semaphore.
connection pools, thread pools
💾
Operating System Kernels
Linux uses spinlocks for short critical sections in interrupt handlers, mutexes for longer ones, RCU for lockless read-heavy paths.
spinlock_t, mutex_t, rwlock_t
📂
File System Journaling
ext4, NTFS, APFS use synchronization to serialise metadata updates. Without it a crash mid-write would corrupt the filesystem.
journal locks, i-node mutexes
💰
Payment Systems
Bank ledgers and blockchain double-spend prevention rely on strict serialisation of account updates. Section 01's story in production.
two-phase commit, consensus
🎮
Game Engines
Multi-threaded engines synchronize physics, rendering, and AI updates each frame using barriers, condition variables, and lock-free queues.
Unity Jobs, Unreal Task Graph

Section 14

Golden Rules — Process Synchronization

🔑 Galvin's Non-Negotiable Rules
1
Every access to shared mutable data must be inside a critical section. No exceptions. Even a single unprotected read of a shared multi-word value can produce torn reads on modern hardware.
2
A correct solution to the critical-section problem must satisfy all three: mutual exclusion, progress, and bounded waiting. Any solution missing one is broken.
3
For two processes, Peterson's algorithm is the classical software-only solution. It requires only shared memory and atomic reads/writes, but needs memory barriers on modern CPUs.
4
A semaphore is an integer with two atomic ops: wait() and signal(). Signals are remembered. Use a counting semaphore for a resource pool of size N; use a binary semaphore for pure mutex.
5
In the bounded-buffer problem, always acquire the counting semaphore (empty/full) before the mutex. Reversing this order causes deadlock.
6
Semaphores are error-prone. Whenever possible prefer monitors (Java synchronized, C# lock, Python with lock:). The compiler enforces mutual exclusion so you can't forget a release.
7
Condition variables are stateless — always check the predicate in a while loop, not an if. This handles spurious wakeups and Mesa-style signalling.
8
To avoid deadlock: acquire locks in a consistent global order. If every process asks for mutex A before mutex B, circular waits are impossible. Prevents 90% of production deadlocks.
9
Hold locks as briefly as possible. Do computation outside the critical section; enter only to update shared state. Long critical sections destroy concurrency.
You have completed Process Synchronization. View all sections →