Requirements for a Correct Critical Section Solution

Imagine two bank-account threads both executing `balance = balance + amount` at the same instant. Without coordination, one thread's update silently overwrites the other's. Money vanishes from the system. This is the Race Conditions and the Critical Section Problem made concrete. It demands a precise fix, not a vague one. Simply saying "make it safe" is not an engineering specification. An operating system needs a checkable definition. It must define what a correct solution to concurrent access must guarantee. That definition has three parts. Every synchronization primitive ever built is judged against it. This includes disabling interrupts on a single-CPU kernel. It also includes a distributed lock service coordinating thousands of machines. The three properties are mutual exclusion, progress, and bounded waiting. They are not three independent wishes. They form a single contract that a critical-section protocol must satisfy simultaneously. Each one rules out a specific class of broken "solutions." These solutions look correct at first glance but fail under some interleaving. This matters because concurrency bugs are notoriously non-reproducible. A solution can pass every test run for months. Then it can corrupt data in production the one time a rare thread interleaving occurs. Having a formal, three-part correctness criterion changes things. It turns "it seems to work" into something you can reason about. You gain an invariant and a proof sketch. This is the same way loop invariants let you prove a sorting algorithm correct. You prove it rather than just running it on sample arrays. This article gives you the exact statement of each requirement. It shows the model of a process's code that the requirements are stated over. This model includes the entry section, critical section, exit section, and remainder section. It walks through a state-machine diagram of two competing processes. Then it stress-tests a naive "solution" against the three criteria. This shows precisely where it breaks. This groundwork is what makes Peterson's Software Solution and hardware-based Locks and Mutual Exclusion provably correct rather than merely plausible.

Key takeaways
  • A correct critical-section (CS) solution must satisfy three properties simultaneously: mutual exclusion, progress, and bounded waiting.
  • Mutual exclusion: at most one process may execute inside its critical section at any instant.
  • Progress: if no process is in its CS, the decision of who enters next cannot be postponed forever by processes not trying to enter.
  • Bounded waiting: there is a finite upper bound on how many times other processes can enter their CS before a waiting process gets its turn.
  • Each property blocks a different failure mode: mutual exclusion blocks corruption, progress blocks a self-inflicted deadlock/livelock, bounded waiting blocks starvation.
  • These three properties are the acceptance test used to prove or refute any concrete algorithm, including Peterson's algorithm and hardware locks.
The core insight

A critical-section protocol is correct only if it guarantees mutual exclusion, progress, and bounded waiting all at once — dropping any single one reopens a distinct failure mode.

The idea

Every process that touches a shared resource is modeled as repeatedly cycling through four sections of code: the entry section, where it requests permission to enter; the critical section (CS), where it accesses the shared resource; the exit section, where it releases permission; and the remainder section, everything else the process does and which is irrelevant to synchronization. A solution to the critical-section problem is an implementation of the entry and exit sections — using shared variables, hardware instructions, or OS primitives — such that no matter how the operating system interleaves instructions from competing processes, three properties always hold.

Mutual exclusion. If process PiP_i is executing in its critical section, then no other process PjP_j (j≠ij \ne i) may be executing in its critical section at the same time. This is the safety property: it is what prevents the lost-update bug in the bank-balance example. Mutual exclusion alone, however, is cheap to satisfy in a useless way — a solution where no process is ever allowed to enter its CS trivially satisfies it while providing no service at all.

Progress. If no process is currently executing in its critical section, and some processes wish to enter their critical sections, then the selection of which process enters next cannot be postponed indefinitely — and, crucially, only processes that are not in their remainder section may participate in that decision. This is a liveness property that rules out both deadlock (nobody ever decides) and any protocol that lets an uninterested, non-competing process block the decision forever.

Bounded waiting. There exists a fixed, finite bound NN on the number of times other processes are allowed to enter their critical sections after a process PiP_i has signaled its intent to enter (by executing its entry section) and before PiP_i's own request is granted. This rules out starvation: a scenario where mutual exclusion and progress both hold globally, yet one unlucky process is perpetually skipped in favor of others, waiting forever even though the system as a whole keeps making progress. Note the subtle but important distinction between progress and bounded waiting. Progress is a global property — the system as a whole is not stuck. Bounded waiting is an individual fairness guarantee — no single process is starved even while the system moves along. A solution can have progress without bounded waiting (the system keeps admitting someone, but always skips P3P_3), so both must be checked independently.

How it works

There is no single "algorithm" for the requirements themselves — they are the specification a mechanism must satisfy. But it is useful to see exactly how a candidate protocol is checked against them, because that check is the algorithmic core of this topic: a systematic interleaving analysis. Below is the general shape of any candidate entry/exit protocol, followed by the verification procedure used to accept or reject it. ``` // General shape of process P_i in a critical-section protocol process P_i: loop forever: ENTRY_SECTION(i) // may read/write shared control variables CRITICAL_SECTION(i) // access to shared resource EXIT_SECTION(i) // release / signal REMAINDER_SECTION(i) // unrelated computation, may run forever or exit ``` ``` // Generic verification procedure for a candidate protocol // (this is how you PROVE or REFUTE mutual exclusion / progress / bounded waiting) function verify_mutual_exclusion(protocol): for every reachable interleaving of ENTRY/EXIT instructions across all processes: if two or more processes are simultaneously "inside" their CRITICAL_SECTION: return VIOLATED, exhibit the interleaving return HOLDS function verify_progress(protocol): for every reachable state where: no process is inside CRITICAL_SECTION, and at least one process is in ENTRY_SECTION trying to enter: check whether the set of processes in {ENTRY_SECTION, CRITICAL_SECTION} can make a decision (someone eventually enters) using only steps from processes NOT in REMAINDER_SECTION if no such decision is reachable in finite steps: return VIOLATED (deadlock or livelock), exhibit the schedule return HOLDS function verify_bounded_waiting(protocol): for every process P_i and every point where P_i has just executed the first statement of ENTRY_SECTION(i): let count = 0 while P_i has not yet entered CRITICAL_SECTION(i): allow the scheduler to run any other process P_j if P_j enters CRITICAL_SECTION(j): count += 1 if the scheduler can force count -> infinity while P_i is denied entry forever: return VIOLATED (unbounded waiting / starvation) return HOLDS // provided a finite bound N exists uniformly ``` The verification procedure is not something you literally run in production — it is the mental (or formal-methods) exercise of adversarial scheduling: assume the scheduler is a malicious adversary trying to break one of the three properties, and ask whether it can succeed. A protocol is correct only if the adversary cannot, no matter how it interleaves instructions or how many processes are competing. The diagram below shows the state machine each process moves through, and the shared decision point where the three properties are actually tested.

Figure
Per-process state machine and the shared entry gate two processes must pass through safely
Critical Section Cycle — two competing processes P_i Remainder Entry (requesting) Critical Section Exit P_j Remainder Entry (requesting) Critical Section Exit Shared decision gate only ONE ticket admitted

Both processes cycle through the same four sections independently, but the entry sections of PiP_i and PjP_j both funnel into a single shared gate. Mutual exclusion is the claim that the gate never admits both tickets at once; progress is the claim that the gate always resolves a pending decision when the CS is empty; bounded waiting is the claim that the gate cannot keep favoring the same ticket-holder forever.

Complexity & correctness

Space. A software critical-section solution for nn processes typically needs O(n)O(n) shared state — e.g., one flag per process plus O(log⁡n)O(\log n) or O(1)O(1) bits for a turn/ticket variable — because the entry section must be able to distinguish "is PjP_j interested" for every other process jj. Hardware-instruction-based solutions (test-and-set, compare-and-swap) can often collapse this to O(1)O(1) shared state (a single lock word) at the cost of relying on an atomic hardware primitive rather than pure software.

Time. The entry section's time cost is measured in the worst-case number of "busy-wait" iterations or context switches before entry is granted. For the requirements themselves, the relevant complexity claim is the bound NN in bounded waiting: a correct solution must have N=O(n)N = O(n) in the number of competing processes (in the strongest formulations, at most one turn per other process before you are served), never O(∞)O(\infty). An algorithm that satisfies mutual exclusion and progress but only offers an unbounded, schedule-dependent wait is not a correct solution by this specification, even though naive testing might never reveal the flaw.

Correctness argument — why all three are independently necessary. The cleanest way to see that the three properties are logically independent (none implies another) is to exhibit, for each property, a "solution" that satisfies the other two but violates it. Violating mutual exclusion while keeping progress and bounded waiting: let every process simply enter its CS whenever it wants, with an empty entry section (`ENTRY_SECTION` does nothing). Progress holds trivially (nothing ever blocks a decision) and bounded waiting holds trivially (N=0N = 0, no one ever waits) — but two processes can be in the CS simultaneously. This is exactly the race condition the whole problem exists to prevent, so mutual exclusion cannot be dropped. Violating progress while keeping mutual exclusion and (vacuous) bounded waiting: let `ENTRY_SECTION` be `while (true) {}` — an unconditional infinite loop before anyone is ever admitted. Mutual exclusion holds vacuously (no process is ever in the CS, so certainly not two at once), and bounded waiting holds vacuously (no process ever does enter, so there is nothing to count, and some formalizations trivially call this "bounded" by 0 for lack of any admitted process) — but the system is globally deadlocked. This shows mutual exclusion alone is a hollow guarantee; you must forbid this "safe-but-useless" solution, which is exactly what progress does. Violating bounded waiting while keeping mutual exclusion and progress: use a single shared lock with a scheduler that always grants the lock to whichever waiting process has the lowest process ID whenever the lock is free. Mutual exclusion holds (the lock enforces it), and progress holds (whenever the CS is empty and someone is waiting, the decision is made immediately). But process P100P_{100} can be starved forever if P1,…,P99P_1, \dots, P_{99} keep re-requesting the lock; there is no finite bound on how many times other processes enter before P100P_{100} does. This shows progress does not imply fairness — bounded waiting must be stated and enforced separately, e.g., via a FIFO ticket or queue. Together these three counterexamples form a minimality proof: each property forbids a distinct, realizable failure that the other two do not forbid. A correct solution is precisely one for which no such counterexample can be constructed — formally, one for which the three `verify_*` procedures above all return `HOLDS` for every process count and every fair scheduler.

Honesty about assumptions. The classical formulation assumes a scheduler that treats each line of code (or memory access) as an indivisible step, and it typically assumes no process halts inside its critical section and that remainder-section execution may take arbitrarily long but is not itself part of the contention. Real hardware complicates this with caching, instruction reordering, and interrupts — which is why Locks and Mutual Exclusion rely on atomic instructions and memory barriers rather than plain reads/writes, and why software-only proofs like Peterson's Software Solution must state explicit assumptions about atomic memory access.

Worked example

Consider a tempting but broken "solution" using a single shared integer `turn`, for two processes P0P_0 and P1P_1: ``` shared int turn = 0; process P_i (i is 0 or 1; j is the other process): loop forever: while (turn != i) { } // ENTRY: busy-wait for my turn CRITICAL_SECTION turn = j; // EXIT: hand the turn to the other REMAINDER_SECTION ``` Let's check this against all three properties with an explicit trace.

StepAction`turn`State
0Initial0P0P_0 eligible to enter
1P0P_0 passes `while`, enters CS0P0P_0 in CS
2P0P_0 exits, sets `turn = 1`1CS empty
3P1P_1 passes `while`, enters CS1P1P_1 in CS
4P1P_1 exits, sets `turn = 0`0CS empty

Mutual exclusion clearly holds: `turn` can only equal one value at a time, so only the process matching it can pass the `while` check, and the CS never has two occupants — this part of the logic is sound. Now stress-test progress. Suppose P1P_1 has no more work to do right now and is sitting in its (possibly long) remainder section, while P0P_0 finishes its CS and sets `turn = 1`. P0P_0 immediately wants to re-enter its CS, but `turn == 1` and P1P_1 is not currently interested in entering at all. P0P_0 is forced to busy-wait indefinitely even though no process is in the critical section and P0P_0 is the only one trying to enter. This is a direct violation of progress: the decision of who enters is being postponed by a process (P1P_1) that is not competing (it's in its remainder section), which is exactly the scenario progress forbids.

TimeP0P_0P1P_1`turn`Verdict
t0t_0exits CS, sets turn=1in remainder (not requesting)1CS empty
t1t_1wants to re-enter, busy-waitsstill in remainder1P0P_0 blocked
t2t_2still busy-waitingstill in remainder, indefinitely1Progress violated

So this "turn" solution is not a correct critical-section solution — it strictly alternates access and therefore silently assumes both processes always want to enter equally often, which the progress requirement explicitly disallows it from assuming. This single worked counterexample is the standard way textbooks motivate why Peterson's Software Solution needs a second variable (a `flag[]` array) in addition to `turn`, precisely to let a process enter twice in a row when the other process is not interested.

Common pitfalls & traps

PitfallWhy it's wrong
Treating "no crash observed in testing" as proof of mutual exclusionRace windows can be nanoseconds wide; absence of an observed bug is not absence of an interleaving that causes one
Confusing progress with bounded waitingA solution can globally keep moving (progress) while starving one specific process forever (no bounded waiting) — they must be checked separately
Forgetting that remainder-section processes must be excluded from the progress decisionIf an idle process can block others' entry decision, that is a progress violation, not acceptable behavior
Assuming strict alternation (`turn`-only schemes) is fineStrict alternation violates progress whenever one process temporarily has no need to enter, as shown in the worked example
Ignoring atomicity of the underlying read/writeSoftware proofs (e.g., Peterson's) assume individual memory reads/writes are atomic; on real hardware with caching/reordering this needs memory barriers
Treating a lock that "always works in practice" as satisfying bounded waitingA LIFO or lowest-ID-first scheduling policy can satisfy mutual exclusion and progress yet starve some process indefinitely

Pioneers & history

The formal three-property specification of the critical-section problem, and the first constructive proofs for it, emerged from foundational work on process synchronization in the 1960s, as multiprogrammed operating systems made concurrent access to shared memory a first-class engineering problem rather than a theoretical curiosity.

Origins · 1 minEdsger W. Dijkstra and how this was discovered
EW
Edsger W. Dijkstra
1930–2002 · Dutch computer scientist; pioneered structured concurrency, semaphores, and formal correctness reasoning for parallel programs

History
1965
— Dijkstra publishes the first general software solution to mutual exclusion for nn processes and formalizes the problem.
1966
— Dijkstra introduces semaphores as a general synchronization primitive built to satisfy these properties directly.
1974
— Peterson's two-process algorithm gives a much simpler proof that mutual exclusion, progress, and bounded waiting can all be achieved in software.
1979
— Lamport's Bakery Algorithm generalizes bounded waiting guarantees to nn processes using a ticket-based ordering.
1980
s — Hardware vendors add atomic test-and-set and compare-and-swap instructions, moving mutual exclusion partly into silicon.

Where this matters

Every operating system kernel enforces mutual exclusion, progress, and bounded waiting (or explicitly documents where it relaxes one of them) when protecting scheduler queues, page tables, and I/O buffers from concurrent modification by multiple CPUs or interrupt handlers. Database engines rely on the same three-property contract when implementing row-level and table-level locks so that concurrent transactions don't corrupt data yet no transaction is starved forever. Multi-threaded language runtimes — the JVM's `synchronized` blocks, POSIX `pthread_mutex`, Rust's `Mutex` — are all built from lower-level hardware locks that are engineered and proven against exactly this specification. Distributed systems extend the same idea to distributed mutual exclusion (e.g., Chubby, ZooKeeper, etcd), where "bounded waiting" becomes a fairness guarantee on lease acquisition across machines, and "progress" becomes the liveness property that the cluster keeps making decisions even if some nodes are slow. Understanding these three requirements precisely is the prerequisite for correctly analyzing Peterson's Software Solution, hardware-instruction-based Locks and Mutual Exclusion, and later, semaphores and monitors.

Ask the tutor: Can you show why Peterson's Solution's second variable (the `flag[]` array) is exactly what fixes the progress violation in the strict-alternation example?

Why isn't mutual exclusion alone a sufficient definition of "correct"?

Mutual exclusion only forbids two processes from being in the critical section simultaneously; it says nothing about whether anyone ever gets in at all. A protocol where every process's entry section is an infinite loop that never lets anyone proceed trivially satisfies mutual exclusion — no two processes are ever in the CS, because no process is ever in the CS. That "solution" is obviously useless: it deadlocks the whole system. This is precisely why progress is required as a second, independent property: it forces the protocol to actually make forward-moving decisions whenever the critical section is free and someone wants in, ruling out this class of hollow-but-safe non-solutions.

What's the practical difference between progress and bounded waiting?

Progress is a system-wide liveness guarantee: whenever the critical section is empty and at least one process wants in, some process (not necessarily any particular one) is admitted in finite time — the system as a whole never freezes. Bounded waiting is a per-process fairness guarantee: for any specific process that starts waiting, there is a finite cap on how many times other processes can cut in line before it. A system can have progress (it keeps admitting somebody) while still starving one unlucky process forever if the scheduling policy always favors others — that violates bounded waiting even though progress holds. Correct solutions must guarantee both.

Can a solution satisfy bounded waiting but violate mutual exclusion?

Yes, trivially, and this shows the properties are logically independent, not a hierarchy. Take a protocol where every process is admitted to the critical section immediately, with no gate at all: since nobody ever waits, the "bound" on waiting is vacuously zero, satisfying bounded waiting in the weakest technical sense. But nothing prevents two processes from executing in the critical section at the same time, so mutual exclusion is violated outright. This is why all three properties must be checked as a conjunction — satisfying any subset of them, even two out of three, does not constitute a correct critical-section solution.

Do these three properties assume a specific number of processes?

The classical statement is usually given for a general nn, and a correct solution must hold for that entire family, not just n=2n = 2. Some historically important algorithms, like the original two-process alternation scheme, only work for exactly two processes and break (or need modification) when generalized. Peterson's algorithm is famous partly because it gives a clean two-process proof, while the Bakery algorithm generalizes bounded waiting to arbitrary nn using a ticket-number ordering, at the cost of higher shared-memory usage. When evaluating any protocol, always check which nn the correctness proof was actually established for.

Why do software-only proofs assume memory reads and writes are atomic?

The three-property proofs are typically carried out in a model where each individual read or write to a shared variable is indivisible — no other process can observe a "half-written" value. This assumption is what allows a clean interleaving argument: you can enumerate the finite set of ways individual reads/writes from different processes can be ordered and check each one. Real modern hardware, however, has caches, store buffers, and instruction reordering by the compiler and CPU, so a variable write by one core is not automatically and immediately visible to another core. This is why real implementations pair software logic with memory barriers or rely on hardware-guaranteed atomic instructions rather than trusting plain variable assignments to behave atomically.

How do hardware instructions like test-and-set change this picture?

Hardware atomic instructions (test-and-set, compare-and-swap) collapse the entry section to a single indivisible operation performed by the CPU itself, rather than a multi-step software sequence a scheduler could interleave mid-way. This makes mutual exclusion far easier to prove, because there is no window in which two processes can both "see" the lock as free. However, a naive spin-loop built directly on test-and-set typically still fails bounded waiting — the hardware guarantees atomicity of the check-and-set step, but says nothing about scheduling fairness among the processes contending for it, so an explicit queueing or ticket mechanism is usually layered on top.

Is disabling interrupts a valid way to satisfy all three properties?

On a single-CPU system, disabling interrupts during the critical section does guarantee mutual exclusion, since the running process cannot be preempted mid-CS. It also tends to give trivial progress and bounded waiting in the simplest single-core case, since execution is effectively serialized. But this approach fails outright on multiprocessor systems, since disabling interrupts on one CPU does nothing to stop a process running on a different CPU from entering the critical section simultaneously. It is also a heavy-handed tool in practice, since it blocks all interrupt-driven work (timers, I/O completions) system-wide, not just contention for one specific resource.

Before this

Where this leads