Introduction
When two threads update shared data at the same time, the result can depend on the exact order in which their machine instructions interleave. That situation is a race condition. The statement count = count + 1 looks indivisible in C, but it compiles to a load, an add and a store; if two threads interleave those steps, one update can be lost.
The cure is to make each update a critical section: a piece of code that only one thread may execute at a time. The critical-section problem is to design entry and exit code around the critical section that guarantees this, without deadlock and without making anyone wait forever. The three requirements are stated precisely and then used to check each classic attempt: two-process software solutions (strict alternation, flag-based attempts, Peterson's algorithm), disabling interrupts, and the hardware instructions TestAndSet, swap and compare-and-swap. Semaphores and monitors, which build on these, are in Semaphores, Mutexes and Monitors.
Prerequisites
- Threads and Multithreading Models: what threads share, and why globals and the heap are shared.
Core concepts
A race condition, exhaustively
Two threads each execute x = x + 1 once, starting from x = 0, as three atomic steps: r <- x, r <- r + 1, x <- r (each thread has its own register r). If one thread finishes before the other starts, x ends at 2. If both load before either stores, both store 1 and x ends at 1. Over all interleavings the possible final values are exactly .
If each thread does the increment twice, the possible final values are ; three times each gives . The minimum is 2, not , for increments per thread: thread A loads 0 and stalls; B does all but its last increment; A stores 1; B loads 1 for its last round and stalls; A finishes all its increments; B finally stores 2. Three threads doing one increment each can end at 1, 2 or 3.
Structure and requirements
Each process repeats:
loop
entry section
critical section
exit section
remainder sectionA solution must satisfy:
- Mutual exclusion. At most one process is in its critical section at any time.
- Progress. If no process is in its critical section and some want to enter, only processes not in their remainder sections take part in deciding who enters next, and the decision cannot be postponed forever. In practice: no deadlock among the contenders, and a process that does not want to enter cannot block one that does.
- Bounded waiting. After a process has requested entry, there is a bound on how many times other processes may enter before it does. This excludes starvation.
Assumptions: each process runs at a non-zero speed, nothing is assumed about relative speeds, and individual loads and stores are atomic.
Progress versus bounded waiting. Progress is about the system as a whole (somebody gets in); bounded waiting is about each individual (every requester gets in after a bounded number of others). A solution can have progress and still starve one process, as a TestAndSet lock does.
Two-process software solutions
The processes are and ; in the code, is the current process and the other. All variables shown are shared.
Attempt 1: strict alternation
shared turn <- 0
entry: while turn != i do nothing
exit: turn <- jMutual exclusion holds (only the process named by turn may enter). Progress fails: if leaves its critical section and stays in its remainder forever, can enter once more and then waits forever for a turn that never comes, although the critical section is free. Bounded waiting holds with bound 1.
Attempt 2: set the flag, then check
shared flag[0..1] <- false
entry: flag[i] <- true
while flag[j] do nothing
exit: flag[i] <- falseMutual exclusion holds. Progress fails through deadlock: if both set their flags before either tests, both spin forever.
Attempt 3: check, then set the flag
entry: while flag[j] do nothing
flag[i] <- true
exit: flag[i] <- falseMutual exclusion fails: both can pass the while (each sees the other's flag false) and then both set their flags and enter.
Peterson's algorithm
shared flag[0..1] <- false, turn
entry: flag[i] <- true
turn <- j
while flag[j] and turn = j do nothing
exit: flag[i] <- falseflag[i] says "I want to enter"; turn <- j says "but you may go first". If both want in, turn holds whichever value was written last, and the process that wrote last waits.
- Mutual exclusion. Assume sequentially consistent atomic reads/writes. If one process observes the other flag false and enters first, the later process sets its flag and writes turn naming the first; with the first still inside, its true flag blocks the later entrant. If both flags are already true, consider the last write to turn: its writer names the other process and cannot pass its loop while that other flag remains true. Thus both cannot be inside simultaneously; comparing two observations at different times is not enough by itself.
- Progress. A process spins only if the other's flag is true and
turnnames the other. A process in its remainder has its flag false, so it cannot block anyone; if both are spinning,turnlets one through. - Bounded waiting with bound 1. If is spinning and leaves the critical section and tries again, sets
turn <- iand now waits itself. So enters at most once while waits.

The order of the two assignments matters. If a process writes turn <- j before flag[i] <- true, mutual exclusion fails: writes turn <- 1; writes turn <- 0, sets its flag and enters because flag[0] is still false; then sets its flag, sees turn = 0 and enters too.
Related material. On modern processors with store buffers and compilers that reorder memory operations, Peterson's algorithm as written is not guaranteed to work without memory barriers. The traces assume sequential consistency.
| Algorithm | Mutual exclusion | Progress | Bounded waiting |
|---|---|---|---|
Strict alternation (turn only) | Yes | No | Yes (bound 1) |
| Set flag, then check | Yes | No (deadlock) | Yes (vacuous when stuck) |
| Check, then set flag | No | Yes | No |
| Peterson | Yes | Yes | Yes (bound 1) |
| Peterson with the two assignments swapped | No | Yes | Yes |
| TestAndSet or swap spin lock | Yes | Yes | No |
For processes, the bakery algorithm (each process takes a number, lowest number enters, ties broken by process ID) satisfies all three requirements in software.
Hardware support
Disabling interrupts
On a single processor, a process can disable interrupts before its critical section and re-enable them after: in this single-CPU kernel model, masking ordinary interrupts prevents interrupt-driven preemption inside the section. Drawbacks: it is a privileged operation, so only the kernel can use it; it does not work on multiprocessors (another CPU still runs); and a long critical section delays all interrupts.
Atomic instructions
The hardware provides instructions that read and modify a memory word in one indivisible step.
TEST-AND-SET(target): -- atomic
old <- target
target <- true
return old
entry: while TEST-AND-SET(lock) do nothing
exit: lock <- falseCOMPARE-AND-SWAP(value, expected, new): -- atomic
old <- value
if old = expected then value <- new
return old
entry: while COMPARE-AND-SWAP(lock, 0, 1) != 0 do nothing
exit: lock <- 0Swap (exchange) is equivalent: each process sets a local key <- true and repeats SWAP(lock, key) until key comes back false.
These simple locks give mutual exclusion (only the caller that saw false enters) and progress, but not bounded waiting: a process can lose the race every time the lock is released, while others re-acquire it forever. A bounded-waiting version keeps an array waiting[0..n-1]: on exit, the releasing process scans the array cyclically from and hands the lock directly to the next waiting process, so each waits at most turns.
Busy waiting and spinlocks
All the solutions so far busy-wait: the waiting process loops, using CPU time. Such a lock is a spinlock. Spinning is wasteful on one CPU (the lock holder cannot run while the spinner holds the CPU, until a timer interrupt) but efficient on multiprocessors when critical sections are short, because it avoids two context switches. Blocking primitives (semaphores with a waiting queue) are the alternative.
Priority inversion. Under strict priority scheduling on one CPU, a high-priority process spinning on a lock held by a low-priority process can spin forever (Tanenbaum calls this the priority inversion problem): the low-priority holder never gets the CPU to release it. With blocking locks, a medium-priority process can keep the low-priority holder off the CPU indefinitely; priority inheritance solves this by temporarily raising the holder's priority.
Worked examples
Classifying an entry protocol
A protocol uses flag[i] <- true; while flag[j] do { flag[i] <- false; flag[i] <- true }. Mutual exclusion holds (a process enters only after seeing the other's flag false while its own is true). Progress is not guaranteed: both processes can keep lowering and raising their flags in lockstep and never see the other's flag false, a livelock. This is the same failure as attempt 2, made less likely but not impossible.
How many times can the other process enter?
In Peterson's algorithm, has executed turn <- 1 and is spinning. is in its critical section. leaves, loops back, sets flag[1] <- true and turn <- 0, and now spins because flag[0] is true and turn = 0. So entered at most once while waited. With a TestAndSet lock, there is no such bound: can release and re-acquire the lock between two of 's tests indefinitely.
Tracing each failed attempt
A claim that a protocol fails is only as good as the interleaving that shows it. Each trace lists the steps in the order they execute; and run the same code.
A single lock variable.
shared lock <- 0
entry: while lock = 1 do nothing
lock <- 1
exit: lock <- 0| Step | Action | lock | In critical section |
|---|---|---|---|
| 1 | tests lock = 1: false | 0 | none |
| 2 | tests lock = 1: false | 0 | none |
| 3 | sets lock <- 1, enters | 1 | |
| 4 | sets lock <- 1, enters | 1 | , |
Mutual exclusion fails: the test and the set are two separate steps, which is exactly the gap that TestAndSet closes. Progress holds (a free lock is always taken by some tester), and bounded waiting fails (one process can win every race).
Attempt 2 (set the flag, then check), deadlock. sets flag[0] <- true; sets flag[1] <- true; tests flag[1]: true, spins; tests flag[0]: true, spins. The critical section is free and neither will ever enter.
Attempt 3 (check, then set the flag), both inside. tests flag[1]: false; tests flag[0]: false; sets its flag and enters; sets its flag and enters.
A variant of Peterson that fails. A student writes turn <- i instead of turn <- j but keeps the loop while flag[j] and turn = j.
| Step | Action | flag[0], flag[1], turn | Result |
|---|---|---|---|
| 1 | : flag[0] <- true | 1, 0, 0 | |
| 2 | : turn <- 0 | 1, 0, 0 | |
| 3 | tests flag[1] and turn = 1: false | 1, 0, 0 | enters |
| 4 | : flag[1] <- true | 1, 1, 0 | |
| 5 | : turn <- 1 | 1, 1, 1 | |
| 6 | tests flag[0] and turn = 0: false | 1, 1, 1 | enters too |
The last writer of turn now goes first instead of waiting, so mutual exclusion fails. If the loop is also changed to while flag[j] and turn = i, the last writer waits again and the protocol is Peterson's algorithm with the meaning of turn inverted: all three requirements hold, with bound 1.
Bounded waiting with TestAndSet. The simple TestAndSet lock gives no bound, as shown above. A standard fix hands the lock directly to a waiting process:
shared waiting[0..n-1] <- false, lock <- false
entry: waiting[i] <- true
key <- true
while waiting[i] and key do key <- TEST-AND-SET(lock)
waiting[i] <- false
exit: j <- (i + 1) mod n
while j != i and not waiting[j] do j <- (j + 1) mod n
if j = i then lock <- false
else waiting[j] <- falseA process enters either because its TestAndSet returned false (the lock was free) or because the exiting process cleared its waiting entry, passing the lock on without releasing it. The exit scan visits processes in cyclic order , so a waiting process is reached after at most other entries. The bound is tight: if all other processes are waiting, the scan can admit each of them once first.
Important properties and observations
- Mutual exclusion alone is easy (never let anyone in); the difficulty is getting all three properties together.
- A solution must work for every interleaving; one bad interleaving is enough to break it.
- The last writer of
turnwaits in Peterson's algorithm. - Atomic instructions make mutual exclusion easy on multiprocessors; fairness needs extra structure.
- Deadlock (nobody moves) and livelock (everybody moves, nobody progresses) both violate progress.
Common misconceptions.
- "
x++is atomic." It is a load, an add and a store unless the hardware and language guarantee otherwise. - "TestAndSet locks prevent starvation." They do not guarantee bounded waiting.
- "Disabling interrupts works on any machine." Only on a uniprocessor, and only in kernel mode.
GATE examination perspective
- Classify a protocol. For each candidate, look for (1) an interleaving that puts both in the critical section, (2) a state where the section is free but nobody can enter (deadlock, or a process in its remainder blocking others), (3) a way for one process to be overtaken without bound.
- Possible values of a shared variable. Break each statement into load, compute and store, and look for the extreme interleavings; for two threads each incrementing times, the range is 2 to .
- Peterson details. Order of
flagandturnwrites; the last writer ofturnwaits; bound 1. - Hardware instructions. TestAndSet and swap: mutual exclusion and progress, no bounded waiting. Disabling interrupts: uniprocessor kernel code only.
- Busy waiting. Spinlocks waste CPU on a uniprocessor and can spin forever under strict priority scheduling.
Summary
Sources
- A. Silberschatz, P. B. Galvin, G. Gagne, Operating System Concepts, 10th ed., Wiley: Chapter 6 (6.1 Background, 6.2 The Critical-Section Problem, 6.3 Peterson's Solution, 6.4 Hardware Support for Synchronization).
- A. S. Tanenbaum, H. Bos, Modern Operating Systems, 4th ed., Pearson: Sections 2.3.1 Race Conditions, 2.3.2 Critical Regions, 2.3.3 Mutual Exclusion with Busy Waiting (disabling interrupts, lock variables, strict alternation, Peterson's solution, the TSL instruction).