Loading content...
Throughout this module, we've discussed the critical section problem and the structure of solutions (entry, critical, exit, and remainder sections). But how do we know if a proposed solution is correct? What criteria must it satisfy?
This question is not merely academic. Many solutions that appear correct on first inspection contain subtle flaws that emerge only under specific timing conditions. To rigorously evaluate synchronization mechanisms, computer scientists have formalized three requirements that any correct solution must satisfy: Mutual Exclusion, Progress, and Bounded Waiting.
These requirements are the litmus test for correctness. A solution that fails even one of them is fundamentally flawed and cannot be safely used in production systems.
By the end of this page, you will understand each of the three requirements in rigorous detail, see examples of violations, learn how to prove that a solution satisfies (or violates) each requirement, and understand why all three are necessary—not just desirable—for correct synchronization.
Formal Definition:
If process P_i is executing in its critical section, then no other process P_j (where j ≠ i) can be executing in its critical section for the same shared resource.
This is the primary requirement—the very reason critical sections exist. Without mutual exclusion, the entire purpose of synchronization is defeated.
What Mutual Exclusion Guarantees:
Note that the requirement is 'at most one' process, not 'exactly one.' If no process wants the critical section, zero processes are in it—and that's perfectly acceptable. Mutual exclusion doesn't require that someone always be in the CS; it requires that no more than one be there simultaneously.
Visualizing Mutual Exclusion:
Consider a timeline where process executions are shown as bars. Mutual exclusion means the CS bars never overlap:
Mutual Exclusion Violation Example:
The following algorithm violates mutual exclusion. Both processes can end up in the CS simultaneously:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546
// BROKEN: This algorithm violates mutual exclusionint turn = 0; // 0 or 1 void enter_cs(int process_id) { while (turn != process_id) { // Wait until it's my turn } // Enter critical section} void exit_cs(int process_id) { turn = 1 - process_id; // Give turn to other process} // WHY IT'S BROKEN:// Consider the scenario where process 0 runs first:// 1. Process 0: turn = 0, so while-loop exits. In CS.// 2. Process 0: Finishes CS. Sets turn = 1.// 3. Process 1: turn = 1, so while-loop exits. In CS.// 4. Process 1: Finishes CS. Sets turn = 0.// 5. Process 0: turn = 0, enters CS again.//// Wait - this seems to work! Where's the violation?//// The violation is PROGRESS, not mutual exclusion.// But consider if turn is initially 0 and ONLY process 1 wants CS:// Process 1 waits forever because turn != 1// This is strict alternation - violates progress, not mutex. // Here's an actual MUTEX VIOLATION:int lock = 0; void broken_enter(int pid) { while (lock == 1) { // Wait while locked } lock = 1; // Claim the lock - NOT ATOMIC WITH THE CHECK!} // VIOLATION SCENARIO:// 1. Process A: reads lock = 0, exits while-loop// 2. [Context switch before setting lock = 1]// 3. Process B: reads lock = 0, exits while-loop// 4. Process A: sets lock = 1, enters CS// 5. Process B: sets lock = 1, enters CS// BOTH ARE IN THE CS! Mutual exclusion violated!Mutual exclusion violations almost always stem from a gap between checking a condition and acting on it. If the check and the action are not atomic, another process can slip through between them. This is why hardware atomic instructions (test-and-set, compare-and-swap) are essential—they make the check-and-act indivisible.
Formal Definition:
If no process is executing in its critical section and one or more processes wish to enter their critical sections, then the selection of the process that will enter the critical section next cannot be postponed indefinitely. Only processes that are not executing in their remainder sections can participate in this decision.
Progress ensures that the system can make forward movement—that work actually gets done. A system that never violates mutual exclusion could still be useless if it also never lets anyone in!
What Progress Guarantees:
| Clause | Meaning | Implication |
|---|---|---|
| "No process is in CS" | The critical section is currently unoccupied | We're not waiting for someone to leave |
| "Some processes wish to enter" | At least one process is in its entry section | There is unfulfilled demand for CS access |
| "Selection cannot be postponed indefinitely" | Eventually, one of the waiting processes will enter | Deadlock on empty CS is impossible |
| "Only non-remainder processes participate" | A process doing other work doesn't block the decision | Disinterested processes can't cause starvation |
Progress Violation Example:
The "strict alternation" algorithm violates progress even though it satisfies mutual exclusion:
123456789101112131415161718192021222324252627282930313233343536373839404142
// BROKEN: This algorithm violates progress (strict alternation)int turn = 0; // 0 = P0's turn, 1 = P1's turn void process_0_entry(void) { while (turn != 0) { // Wait for my turn } // Enter CS} void process_0_exit(void) { turn = 1; // P1's turn next} void process_1_entry(void) { while (turn != 1) { // Wait for my turn } // Enter CS} void process_1_exit(void) { turn = 0; // P0's turn next} // PROGRESS VIOLATION SCENARIO:// Initial: turn = 0// 1. P0 wants CS, enters (turn = 0, loop exits)// 2. P0 finishes, exits, sets turn = 1// 3. P0 immediately wants CS again (short remainder)// 4. P0 calls entry: while (turn != 0)... turn is 1, so P0 WAITS// 5. But P1 is in its remainder section doing something else// 6. CS is EMPTY, P0 WANTS it, but P0 is BLOCKED!//// Why this violates progress:// - No process is in CS (empty)// - P0 wishes to enter (in entry section)// - Yet P0 cannot enter - selection IS being postponed// - P1 is in remainder, so shouldn't block P0's decision//// The algorithm forces alternation even when one process// doesn't want the CS - this is a progress violation.How Peterson's Algorithm Satisfies Progress:
Peterson's algorithm avoids strict alternation by using intention flags in addition to the turn variable:
1234567891011121314151617181920212223242526272829303132
// Peterson's: Satisfies progressint flag[2] = {0, 0}; // Intention flagsint turn; void enter(int i) { int j = 1 - i; flag[i] = 1; // I want in turn = j; // But you go first if you want while (flag[j] == 1 && turn == j) { // Wait only if: // - Other process wants in (flag[j] == 1), AND // - It's their turn (turn == j) }} void exit(int i) { flag[i] = 0; // I'm done, no longer interested} // WHY PROGRESS IS SATISFIED:// Case: CS is empty, P0 wants in, P1 is in remainder//// 1. P1 is in remainder, so flag[1] = 0 (exit set it to 0)// 2. P0 calls enter: sets flag[0] = 1, turn = 1// 3. P0 checks while-loop: flag[1] == 0!// 4. First part of AND is false, loop exits immediately// 5. P0 enters CS without waiting!//// The key insight: The flag variable captures INTENTION.// A process in remainder has flag = 0 (not interested).// An interested process doesn't wait for disinterested ones.Most correct algorithms use some form of 'intention flag'—a mechanism for processes to announce 'I want the CS.' This allows the algorithm to distinguish between 'the CS is free because no one wants it' (progress is trivially satisfied) and 'the CS is free but others are waiting' (need to select one). Without intention flags, you get strict alternation or worse.
Formal Definition:
There must exist a bound, or limit, on the number of times that other processes are allowed to enter their critical sections after a process has made a request to enter its critical section and before that request is granted.
Bounded waiting prevents starvation—the situation where a process waits indefinitely while other processes repeatedly enter the CS.
What Bounded Waiting Guarantees:
Important Distinctions:
Progress vs. Bounded Waiting:
A solution could satisfy progress but violate bounded waiting if it always picks the same process from multiple waiting processes. Progress ensures some process enters; bounded waiting ensures every requesting process enters eventually.
The Bound:
The "bound" in bounded waiting is a maximum number of CS entries by other processes. For example:
12345678910111213141516171819202122232425262728293031323334353637
// BROKEN: Simple spinlock violates bounded waitingint lock = 0; // Assume test_and_set is atomicint test_and_set(int* target) { int old = *target; *target = 1; return old;} void enter(void) { while (test_and_set(&lock) == 1) { // Spin until we get the lock }} void exit(void) { lock = 0;} // BOUNDED WAITING VIOLATION SCENARIO:// Consider 3 processes: A, B, C// // 1. A holds lock, B and C are spinning// 2. A releases lock (sets lock = 0)// 3. B and C race to acquire// 4. A immediately tries to re-acquire (short remainder)// 5. A wins the race (might be on same CPU, faster cache access)// 6. B and C still spinning// 7. Repeat: A releases, A re-acquires, B and C still waiting//// There is NO BOUND on how many times A can enter while B waits!// B could wait forever - this is STARVATION.//// WHY: TAS gives no ordering guarantee. The fastest to respond wins.// On NUMA systems, processes on the same chip as the lock variable// have systematically lower latency and can dominate the lock.How Ticket Locks Satisfy Bounded Waiting:
Ticket locks enforce FIFO ordering, guaranteeing bounded waiting:
123456789101112131415161718192021222324252627282930313233343536373839404142
// Ticket Lock: Satisfies bounded waiting (FIFO fair)volatile int next_ticket = 0; // Next ticket to give outvolatile int now_serving = 0; // Current ticket being served void enter(void) { int my_ticket = atomic_fetch_add(&next_ticket, 1); // Get unique ticket while (now_serving != my_ticket) { // Wait for my number to be called } // Enter CS} void exit(void) { now_serving++; // Call next ticket number} // WHY BOUNDED WAITING IS SATISFIED:// Suppose P_i gets ticket k and is waiting.// - Tickets 0 through k-1 must be served before ticket k// - Each ticket corresponds to one CS entry// - Bound = k = number of processes that requested before P_i// - At most (n-1) other processes can enter before P_i//// PROOF:// 1. All tickets are unique (atomic fetch-add)// 2. Tickets are served in strict numerical order// 3. No process can "skip" to an earlier ticket// 4. Therefore, every waiting process WILL be served// 5. The wait is bounded by the number of tickets ahead//// EXAMPLE:// P0 gets ticket 0, enters CS// P1 gets ticket 1, waits (now_serving = 0)// P2 gets ticket 2, waits (now_serving = 0)// P0 exits, now_serving = 1// P1's wait ends, enters CS (exactly 1 other entry while P1 waited)// P0 re-requests, gets ticket 3, waits! (behind P2)// P1 exits, now_serving = 2// P2 enters (exactly 1 other entry while P2 waited)// ...// Everyone gets served fairly.Starvation can occur even when everything 'looks fair.' On NUMA systems, processes close to the lock variable respond faster when it's released. On systems with priority scheduling, lower-priority processes might be repeatedly preempted just before acquiring the lock. Even with test-and-set, starvation requires specific conditions that might be rare—but 'rare' is not 'impossible.' Production systems must guarantee bounded waiting.
Some might wonder if all three requirements are truly necessary, or if satisfying one or two might be sufficient. The answer is unequivocal: all three are independently necessary, and none implies the others.
Let's examine what happens when each is violated in isolation:
| Violation | What Happens | Example System Failure |
|---|---|---|
| Mutex violation (Progress & BW satisfied) | Multiple processes in CS simultaneously | Bank account debited twice; data structure corruption; security breach |
| Progress violation (Mutex & BW satisfied) | System deadlocks on empty CS; no work done | Thread A waits for B's permission, B is doing unrelated work forever |
| BW violation (Mutex & Progress satisfied) | Some processes starved indefinitely | 99% of requests served promptly; 1% wait forever; support tickets pile up |
Solutions That Satisfy Some But Not All:
In practice, mutual exclusion is considered most critical (incorrect results are unacceptable), followed by progress (deadlock is immediately visible), followed by bounded waiting (starvation may take time to manifest and affect only some users). However, production-quality synchronization primitives must satisfy all three—there's no excuse for leaving any out.
How do we prove that an algorithm satisfies these requirements? This is a crucial skill for understanding and designing synchronization mechanisms. Let's walk through formal proofs for Peterson's Algorithm.
Peterson's Algorithm (Recap):
1234567891011121314151617
// Peterson's Algorithmint flag[2] = {0, 0}; // flag[i] = 1 means process i wants to enterint turn; // Whose turn it is if both want to enter void enter(int i) { int j = 1 - i; // The other process flag[i] = 1; // Line 1: Announce intention turn = j; // Line 2: Give priority to other while (flag[j] == 1 && turn == j) { // Line 3: Busy wait // Wait while other wants in AND it's other's turn } // Line 4: Enter CS} void exit(int i) { flag[i] = 0; // Line 5: Clear intention}Proof of Mutual Exclusion:
We prove by contradiction. Assume both P0 and P1 are in their critical sections simultaneously.
Setup:
flag[1] == 0 OR turn == 0flag[0] == 0 OR turn == 1Case Analysis:
Case 1: One of them saw the other's flag as 0
flag[1] == 0, then P1 hadn't yet executed Line 1flag[1] = 1)flag[1] == 0, P0's read must have happened before P1's writeflag[0] == 1 && turn == 0 — if turn == 0, P1 waitsflag[0] == 1 && turn == 1 — both are true for P1's view!flag[0] == 1 && turn == 0 which is false (turn is 1)Let me redo this more carefully...
Assume both P0 and P1 are in CS
flag[1] == 0 or turn == 0flag[0] == 0 or turn == 1Consider the ordering of Line 2 executions (setting turn):
turn = 1turn = 0Case: P0 writes turn last (turn = 1 is final)
flag[1] == 1 && turn == 1Case: P1 writes turn last (turn = 0 is final)
flag[0] == 1 && turn == 0In both cases, the process that writes turn last must wait.
Conclusion: At most one process can be in CS. Mutual exclusion is satisfied. ∎
The key insight in Peterson's correctness is the turn variable. When both processes want in, the last one to write turn = other defers to the other process. Since writes to turn are totally ordered, exactly one process writes last, and that one waits. The "politeness" of giving the other priority is what breaks the tie.
Proof of Progress:
We must show: if CS is empty and at least one process wants in, someone will enter.
Case 1: Only P0 wants in (P1 is in remainder)
flag[1] = 0 (from P1's last exit)flag[0] = 1, sets turn = 1flag[1] == 0 && turn == 1Case 2: Only P1 wants in (P0 is in remainder)
Case 3: Both P0 and P1 want in
Conclusion: In all cases, someone enters CS when CS is empty and there's demand. Progress is satisfied. ∎
Proof of Bounded Waiting:
We show that if P0 is waiting, P1 can enter at most once before P0 does.
Suppose P0 is waiting in Line 3, meaning: flag[1] == 1 && turn == 1
P1 is in CS and finishes:
flag[1] = 0flag[1] == 0 && turn == 1 — first conjunct is now FALSEBut what if P1 immediately wants in again?
flag[1] = 1, sets turn = 0flag[0] == 1 && turn == 0
flag[1] == 1 && turn == 0
Bound: After P0 starts waiting, P1 can enter at most once (if P1 was already about to enter or in CS). After that, P1 must wait for P0.
Conclusion: Bounded waiting is satisfied with bound = 1. ∎
How do real-world synchronization primitives fare against these requirements? Let's evaluate common mechanisms:
| Primitive | Mutex | Progress | Bounded Waiting | Notes |
|---|---|---|---|---|
| Test-and-Set Spinlock | ✅ | ✅ | ❌ | Simple but unfair; starvation possible under contention |
| Ticket Lock | ✅ | ✅ | ✅ | FIFO fair; wait bounded by queue size |
| MCS Lock | ✅ | ✅ | ✅ | Scalable + FIFO fair; good for NUMA |
| Pthread Mutex | ✅ | ✅ | ⚡ | Implementation-dependent; often fair but not guaranteed by POSIX |
| Peterson's Algorithm | ✅ | ✅ | ✅ | Two-process only; proven correct |
| Bakery Algorithm | ✅ | ✅ | ✅ | N-process software solution; proven correct |
| Semaphore (general) | ✅ | ✅ | ⚡ | Depends on implementation; may or may not be fair |
| Monitor (Java synchronized) | ✅ | ✅ | ❌* | No fairness guarantee; *ReentrantLock can be fair |
| Read-Write Lock | ✅ | ✅ | ⚡ | Often has preferences (readers or writers) |
Key Observations:
Mutual exclusion is universally satisfied—any primitive that doesn't satisfy it is broken and unusable.
Progress is usually satisfied, as long as the algorithm doesn't have structural deadlock issues.
Bounded waiting is the differentiator—fair vs. unfair locks, scalable vs. unscalable. This is often where performance and fairness trade off.
Choosing Based on Requirements:
Many developers assume their mutex is fair because it seems to work. But under high contention, unfair locks can cause severe latency outliers—most requests complete quickly, but some wait 1000x longer. Always check the documentation and test under stress. If fairness is required, use an explicitly fair primitive.
The three classical requirements—mutual exclusion, progress, and bounded waiting—are the foundation. But modern systems often require additional properties:
Additional Properties for Production Systems:
The Trade-Off Space:
No single lock design excels at everything. The design space involves trade-offs:
| Property | Simple TAS | Ticket Lock | MCS Lock | Mutex + Futex |
|---|---|---|---|---|
| Implementation complexity | Very Low | Low | Medium | High |
| Uncontended latency | Very Low | Low | Medium | Low |
| Scalability under contention | Very Poor | Poor | Excellent | Good |
| Fairness | None | FIFO | FIFO | Implementation-dependent |
| Memory per lock | 1 word | 2 words | 2 words + queue | Variable |
| Kernel involvement | None | None | None | On contention |
Choosing the right synchronization primitive requires understanding your specific needs. Questions to ask: How many threads will contend? How long is the critical section? Is fairness required? Is real-time response needed? Is kernel mode acceptable? The answers guide the choice.
The three requirements—mutual exclusion, progress, and bounded waiting—are the formal criteria by which we judge any solution to the critical section problem. Let's consolidate our understanding:
Module Complete:
With this page, we have completed our deep exploration of the Critical Section Problem. You now understand:
This foundation prepares you for the next steps in synchronization: studying specific solutions (Peterson's, Dekker's), hardware primitives (spinlocks, atomic instructions), and higher-level abstractions (semaphores, monitors).
Congratulations! You have mastered the Critical Section Problem—the fundamental challenge of concurrent programming. You understand the four-part process structure, the three correctness requirements, and how to reason about synchronization algorithms. This knowledge is essential for understanding every synchronization mechanism you'll encounter in operating systems, databases, distributed systems, and beyond.