Loading content...
We now possess deep knowledge of the three requirements for correct synchronization solutions: mutual exclusion, progress, and bounded waiting. But knowledge of requirements is incomplete without the ability to evaluate whether a given solution satisfies them.
This page bridges theory and practice. We will develop a systematic framework for analyzing synchronization algorithms—a structured methodology that transforms informal reasoning into rigorous analysis. By the end, you will have a toolkit for evaluating any proposed solution to the critical section problem.
This skill is essential because:
Let us build the analytical framework step by step.
By the end of this page, you will be able to systematically evaluate any synchronization solution against all three requirements, construct adversarial execution traces to find flaws, use state-space analysis for exhaustive verification, and apply evaluation checklists to avoid common analytical mistakes.
Evaluating a synchronization solution requires systematic analysis, not intuition. Here is a structured methodology:
Before analysis, ensure the algorithm is precisely specified:
Ambiguity in any of these leads to incorrect analysis.
1234567891011121314151617181920212223242526272829303132333435363738
// Example: Formalizing a synchronization algorithm // STEP 1: Identify shared variablesShared: bool flag[2] -- Process intention flags int turn -- Turn indicator // STEP 2: Specify initial valuesInitial: flag[0] = false flag[1] = false turn = 0 (or 1, doesn't matter) // STEP 3: Delineate sections (for process i, where j = 1-i) ENTRY SECTION: E1: flag[i] = true E2: turn = j E3: while (flag[j] && turn == j) { } CRITICAL SECTION: CS: <access shared resource> EXIT SECTION: X1: flag[i] = false REMAINDER SECTION: RS: <non-critical work> // STEP 4: Define atomic operationsAtomic: - Reading a single shared variable - Writing a single shared variable - The read + compare in E3's while condition (or if not atomic, analyze as separate operations) // NOTE: Assumptions about atomicity MUST be explicit.// Different assumptions can lead to different conclusions!Attempt to prove that no two processes can be in their critical sections simultaneously.
Approach: Find an invariant that implies mutual exclusion and prove it holds.
Common invariant patterns:
in_cs[i] → lock == true (lock-based)in_cs[i] → some condition uniquely held by i (algorithm-specific)Verify that if the critical section is empty and processes want to enter, one will enter.
Approach: Consider states where:
Show that from any such state, some process eventually enters CS.
Verify that a bound exists on how many times a waiting process can be bypassed.
Approach: Track what happens after a process enters its entry section. Count maximum possible entries by others before this process enters.
Explicitly state all assumptions:
Always check mutual exclusion first. If mutual exclusion fails, the algorithm is fundamentally broken—there's no point analyzing progress or bounded waiting. Progress comes second; bounded waiting builds on progress.
A powerful technique for finding flaws is constructing adversarial execution traces—carefully designed interleavings that attempt to violate each requirement.
Imagine a scheduler determined to break your algorithm:
Your task is to predict what this malicious scheduler would do.
To find a mutual exclusion violation:
123456789101112131415161718192021222324252627282930313233343536373839404142434445
// Demonstrating a mutual exclusion violation via adversarial trace // Flawed algorithm (no turn variable):// Entry: flag[i] = true; while (flag[j]) { }// Exit: flag[i] = false // ADVERSARIAL TRACE: // Initial: flag[0] = false, flag[1] = false // Time | P₀ Action | P₁ Action | flag[0] | flag[1]// -----+---------------------+---------------------+---------+---------// t1 | (in remainder) | flag[1] = true | false | true// t2 | flag[0] = true | (about to check) | true | true// t3 | while(flag[1]): T | while(flag[0]): T | true | true// | ... wait ... | ... wait ... | |//// DEADLOCK! Neither can proceed. But wait, let's try a different trace: // ALTERNATIVE ADVERSARIAL TRACE: // Time | P₀ Action | P₁ Action | flag[0] | flag[1]// -----+---------------------+---------------------+---------+---------// t1 | flag[0] = true | (in remainder) | true | false// t2 | while(flag[1]): F | (in remainder) | true | false// t3 | ENTER CS | flag[1] = true | true | true// t4 | (in CS) | while(flag[0]): T | true | true// t5 | (in CS) | ... wait ... | true | true // OK, this scenario shows deadlock OR one enters first.// But can BOTH enter? Let's try: // MUTUAL EXCLUSION VIOLATION TRACE: // Time | P₀ Action | P₁ Action | flag[0] | flag[1]// -----+---------------------+---------------------+---------+---------// t1 | flag[0] = true | | true | false// t2 | | flag[1] = true | true | true// t3 | while(flag[1]): T | while(flag[0]): T | true | true//// Both wait. No violation yet. This algorithm actually has// a PROGRESS problem (potential deadlock), not mutual exclusion. // The algorithm IS deadlock-prone but DOES satisfy mutual exclusion// (when progress exists, only one enters at a time).To find a progress violation:
1234567891011121314151617181920212223242526272829
// Finding a progress violation: Strict Alternation // Algorithm:// Entry: while (turn != i) { }// Exit: turn = j (where j = 1-i) // ADVERSARIAL TRACE FOR PROGRESS VIOLATION: // Setup: turn = 0, P₀ is in remainder section (long computation) // Time | P₀ Action | P₁ Action | turn// -----+---------------------+---------------------+------// t1 | (in remainder) | wants to enter | 0// t2 | (in remainder) | while(turn != 1): T | 0// t3 | (in remainder) | wait... | 0// t4 | (in remainder) | wait... | 0// ... | (stays in remainder)| wait FOREVER | 0 // ANALYSIS:// - CS is empty (turn = 0 means P₀ could enter, but P₀ isn't trying)// - P₁ wants to enter (in entry section)// - P₁ cannot enter (turn ≠ 1)// - P₀ is in remainder section - not contending // This violates the progress definition:// "only processes NOT in remainder participate in decision"// Here, P₀ in remainder section is blocking P₁'s entry. // CONCLUSION: Progress is violated. Algorithm is incorrect.To find a bounded waiting violation:
Constructing the right trace requires understanding the algorithm's weak points. Start by asking: 'What could go wrong?' For mutual exclusion, look for race conditions in the entry section. For progress, look for dependencies on non-contending processes. For bounded waiting, look for lack of ordering mechanisms.
For exhaustive verification, we can enumerate all reachable states of a synchronization algorithm. This technique, called state-space analysis or model checking, guarantees finding any violations.
A state captures all relevant information about the system:
A transition is an atomic step that moves from one state to another.
1234567891011121314151617181920212223242526272829303132333435363738394041424344
// State-space analysis of a 2-process algorithm // Algorithm (each process i, j = 1-i):// Entry: flag[i] = true; while (flag[j]) { }// Exit: flag[i] = false // STATE REPRESENTATION:// (loc₀, loc₁, flag[0], flag[1])// where loc ∈ {RS, E1, E2, CS, X1} // RS = remainder, E1 = set flag, E2 = check loop, CS = critical, X1 = clear flag // INITIAL STATE: (RS, RS, false, false) // STATE SPACE ENUMERATION: // State 0: (RS, RS, F, F) - initial// P₀ moves to E1: → State 1// P₁ moves to E1: → State 2 // State 1: (E1, RS, F, F) - P₀ about to set flag// P₀ sets flag: → State 3 (E2, RS, T, F)// P₁ moves to E1: → State 4 // State 2: (RS, E1, F, F) - P₁ about to set flag// P₁ sets flag: → State 5 (RS, E2, F, T)// P₀ moves to E1: → State 4 // State 3: (E2, RS, T, F) - P₀ checking, P₁ in remainder// P₀ checks flag[1]=F, enters CS: → State 6 (CS, RS, T, F)// P₁ moves to E1: → State 7 (E2, E1, T, F) // ... continuing enumeration ... // State 8: (E2, E2, T, T) - BOTH checking, BOTH flags true// P₀ checks flag[1]=T, stays in E2: → State 8 (self-loop)// P₁ checks flag[0]=T, stays in E2: → State 8 (self-loop)// // PROBLEM: This is a DEADLOCK state! No progress possible.// Both processes spin forever, neither enters CS. // VERIFICATION RESULTS:// - Mutual exclusion: ✓ (no state has both in CS)// - Progress: ✗ (State 8 is deadlocked)// - Bounded waiting: N/A (no progress means bounded waiting undefined)Mutual Exclusion: Check that no reachable state has two processes in CS.
Progress: Check that from any state where CS is empty and entry sections are occupied, there's a path to a state where someone is in CS.
Bounded Waiting: Check that for any state where Pᵢ is waiting, there's a bound on transitions before Pᵢ enters CS.
The number of states grows exponentially with:
For n processes, each with k locations and v shared variables each of size s:
|States| = k^n × s^v
This limits exhaustive enumeration to small systems. Large systems require:
Tools like SPIN, TLA+, and Promela automate state-space analysis. They're invaluable for verifying synchronization algorithms. For critical systems, formal verification with these tools is standard practice.
To ensure thorough evaluation, use these checklists when analyzing synchronization algorithms.
| Pitfall | Description | How to Avoid |
|---|---|---|
| Atomicity assumption | Assuming multi-step operations are atomic | Explicitly list atomic operations |
| Benevolent scheduler | Assuming scheduler helps correctness | Use adversarial scheduler model |
| Small-scale reasoning | Verifying only 2 processes when n > 2 | Test with maximum process count |
| Confirming intuition | Finding traces that work, not that fail | Try to break the algorithm |
| Memory model ignorance | Ignoring reordering on weak models | State memory model assumptions |
Let's apply our evaluation framework to analyze a synchronization algorithm end-to-end.
Consider this proposed 2-process solution:
1234567891011121314151617181920212223242526272829303132
// Proposed solution for 2-process mutual exclusion // Shared variablesbool flag[2] = {false, false}; // Interest flagsint turn = 0; // Priority indicator // Process i (where j = 1 - i)void process(int i) { int j = 1 - i; while (true) { // ENTRY SECTION flag[i] = true; // Line E1: Declare interest while (flag[j]) { // Line E2: While other interested if (turn == j) { // Line E3: If it's their turn flag[i] = false; // Line E4: Withdraw temporarily while (turn == j) { } // Line E5: Wait for my turn flag[i] = true; // Line E6: Re-declare interest } } // CRITICAL SECTION critical_section(); // EXIT SECTION turn = j; // Line X1: Give turn to other flag[i] = false; // Line X2: Clear interest // REMAINDER SECTION remainder_section(); }}Shared variables: flag[0], flag[1], turn
Initial values: flag[0] = false, flag[1] = false, turn = 0
Atomic operations: Single reads and writes
Process locations: RS, E1-E6, CS, X1, X2
12345678910111213141516171819202122232425262728293031323334353637383940414243444546
// MUTUAL EXCLUSION ANALYSIS // Claim: No state has both processes in CS simultaneously. // Proof approach: Show invariant I holds, and I → ¬(in_cs[0] ∧ in_cs[1]) // Key observation: To enter CS, process i must:// 1. Set flag[i] = true// 2. Find flag[j] = false (exit the while loop at E2) // Invariant attempt:// I: in_cs[i] → flag[i] = true // If both in CS:// - flag[0] = true (by invariant, since in_cs[0])// - flag[1] = true (by invariant, since in_cs[1]) // But to enter CS, each must have seen the other's flag as false!// // Case analysis:// - P₀ saw flag[1] = false at E2, then entered CS// - P₁ saw flag[0] = false at E2, then entered CS// - But once in CS, both have flags true// // For both to enter, one must have checked BEFORE the other set flag:// Sequence: P₀ checks flag[1]=F → P₁ sets flag[1]=T → P₁ checks flag[0]// But wait, P₀ set flag[0]=T at E1, so P₁ sees flag[0]=T and doesn't enter!// // Actually, be careful about the deference mechanism (E3-E6).// Let's trace more carefully... // If P₀ clears flag at E4, then P₁ might see flag[0]=false...// // Detailed trace attempt:// P₀: E1 (flag[0]=T), E2 (flag[1]?), sees T, E3 (turn==1?), ...// P₁: E1 (flag[1]=T), E2 (flag[0]?), sees T, E3 (turn==0?), ...// // turn is either 0 or 1.// If turn = 0: P₁ defers (E4: flag[1]=F, E5: wait), P₀ passes E2, enters CS// If turn = 1: P₀ defers (E4: flag[0]=F, E5: wait), P₁ passes E2, enters CS// // One process defers, allowing the other to enter.// // CONCLUSION: Mutual exclusion holds ✓ // The turn variable acts as a tiebreaker when both have flags set.12345678910111213141516171819202122232425262728293031
// PROGRESS ANALYSIS // Question: If CS is empty and processes want to enter, can entry happen? // Scenario 1: Only P₀ wants to enter (P₁ in remainder)// - P₀ sets flag[0] = true// - P₀ checks flag[1] = false (P₁ didn't set it while in remainder)// - P₀ exits while loop, enters CS ✓ // Scenario 2: Both want to enter// - Both set their flags true// - Both fail the outer while condition initially (flag[j] = true)// - Both check turn// - Exactly one has turn == j (since turn is 0 or 1)// - That one defers (clears flag, waits for turn change)// - The other sees flag[j] = false, enters CS ✓// - In CS, it eventually exits and sets turn = j// - The deferred process's E5 waiting ends// - It re-sets its flag, proceeds, enters CS ✓ // Scenario 3: Neither currently in entry, but...(remainder independence)// - P₀ in remainder, P₁ wants to enter// - P₁ sets flag[1] = true// - P₁ checks flag[0] = false (P₀ in remainder, flag[0] = false)// - P₁ enters CS ✓// - P₀ being in remainder does NOT block P₁ // CONCLUSION: Progress holds ✓ // No deadlock: The turn variable breaks symmetry.// No remainder dependency: Process in remainder has flag = false.123456789101112131415161718192021222324252627282930313233
// BOUNDED WAITING ANALYSIS // Question: After P₀ requests entry, can P₁ enter unboundedly often? // Trace analysis:// 1. P₀ enters entry section (sets flag[0] = true) at time t₀// 2. P₁ is potentially in CS or also entering // Case A: P₁ in CS at time t₀// - P₁ will exit, set turn = 0, set flag[1] = false// - P₀ will see flag[1] = false (if checking E2) or turn = 0 (if in E5)// - P₀ proceeds, enters CS// - P₁ entered 1 time after t₀ (was already in CS)// - Bound: 1 ✓ // Case B: P₁ also entering at time t₀// - Both have flags true, turn is either 0 or 1// - If turn = 1: P₀ defers, P₁ enters, P₁ exits, sets turn = 0// - Now P₀ stops waiting at E5, re-sets flag, checks E2// - P₁ might want to enter again and set flag[1] = true// - P₁ checks turn = 0, so if P₀'s flag is true, P₁ defers// - P₀ enters// - P₁ entered 1 time after t₀// - If turn = 0: P₁ defers, P₀ enters// - P₁ entered 0 times before P₀// - Bound: 0 ✓ // Maximum bypass: 1 (when P₁ was already in CS or had priority) // CONCLUSION: Bounded waiting holds with B = 1 ✓ // The turn variable ensures the waiting process gets priority// after the other process exits.This algorithm (Dekker's algorithm variant) satisfies all three requirements: Mutual Exclusion ✓, Progress ✓, Bounded Waiting with B=1 ✓. It is a correct solution to the 2-process critical section problem.
Let's apply our evaluation framework to compare several synchronization approaches:
| Algorithm | Mutual Excl. | Progress | Bounded Wait | Bound B | Notes |
|---|---|---|---|---|---|
| Disable interrupts | ✓ | ✓* | ✓* | 0 | *Only on uniprocessor; dangerous |
| Strict alternation | ✓ | ✗ | N/A | N/A | Requires alternating access |
| Flag without turn | ✓ | ✗ | N/A | N/A | Can deadlock |
| Peterson's (2-proc) | ✓ | ✓ | ✓ | 1 | Classic correct solution |
| Dekker's (2-proc) | ✓ | ✓ | ✓ | 1 | First correct algorithm |
| Lamport's Bakery | ✓ | ✓ | ✓ | n-1 | Works for n processes |
| TAS spinlock | ✓ | ✓ | ✗ | ∞ | Simple but unfair |
| Ticket lock | ✓ | ✓ | ✓ | n-1 | Fair and practical |
| MCS lock | ✓ | ✓ | ✓ | n-1 | Scalable queue-based |
1. Correctness is binary, not gradual An algorithm is correct (all three properties) or incorrect (missing any). There's no 'partially correct.'
2. Simplicity and correctness can conflict The simplest approaches (strict alternation, basic flags) are often incorrect. Correct algorithms require more sophisticated mechanisms.
3. Practical considerations matter An algorithm might be theoretically correct but impractical (e.g., Lamport's Bakery has unbounded ticket numbers). Real implementations need bounded resources.
4. Context determines choice Different contexts favor different algorithms:
Manual evaluation is essential for understanding, but automated tools extend our verification capabilities.
Model checkers automatically explore the state space and check properties:
SPIN (Simple Promela Interpreter)
TLA+ (Temporal Logic of Actions)
Alloy
1234567891011121314151617181920212223242526272829303132333435363738394041
/* SPIN/Promela model of Peterson's algorithm */ bool flag[2] = false;byte turn = 0; /* Process template */proctype P(byte i) { byte j = 1 - i; do :: /* Entry section */ flag[i] = true; turn = j; (flag[j] == false || turn == i); /* Wait */ /* Critical section */cs: skip; /* Label for property checking */ /* Exit section */ flag[i] = false; od} /* Mutual exclusion property */ltl mutex { []!(P[0]@cs && P[1]@cs) } /* Progress property */ltl progress { [](flag[0] -> <>P[0]@cs) && [](flag[1] -> <>P[1]@cs) } /* Start both processes */init { run P(0); run P(1);} /* To verify: spin -a peterson.pml gcc -o pan pan.c ./pan -a*/Static analyzers check code without running it:
ThreadSanitizer (TSan): Detects data races at runtime (via instrumentation) Helgrind: Valgrind-based race detector RacerD: Facebook's static race detector for Java/C++ Infer: Static analyzer that can find concurrency bugs
While powerful, tools have limitations:
The best approach combines manual reasoning (for understanding and proof development) with automated tools (for exhaustive checking and catching oversights). Use manual analysis to develop invariants and intuition, then encode in a model checker for verification.
We have developed a comprehensive framework for evaluating synchronization solutions. Let's consolidate the key elements.
┌─────────────────────────────────────────────────────────────┐
│ SOLUTION EVALUATION │
├─────────────────────────────────────────────────────────────┤
│ 1. FORMALIZE │ Shared vars, initial values, atomicity │
├───────────────────┼─────────────────────────────────────────┤
│ 2. MUTUAL EXCL. │ No two processes in CS simultaneously │
├───────────────────┼─────────────────────────────────────────┤
│ 3. PROGRESS │ Empty CS + contenders → entry happens │
├───────────────────┼─────────────────────────────────────────┤
│ 4. BOUNDED WAIT │ Finite bypass count for each waiter │
├───────────────────┼─────────────────────────────────────────┤
│ 5. DOCUMENT │ State all assumptions explicitly │
└───────────────────┴─────────────────────────────────────────┘
With this framework, you can confidently evaluate any proposed synchronization solution—distinguishing correct algorithms from flawed ones, and understanding exactly why each succeeds or fails.
You now possess a systematic framework for evaluating synchronization solutions. You can formalize algorithms, construct adversarial traces, perform state-space analysis, and apply evaluation checklists. In the final page, we will explore formal correctness proofs—the rigorous mathematical foundation for verifying synchronization algorithms.