Loading content...
Throughout this module, we have evaluated synchronization algorithms by reasoning about their properties. But reasoning alone leaves room for error. Subtle bugs can hide in informal arguments. The history of computer science is filled with algorithms believed correct for years before subtle flaws were discovered.
This brings us to the gold standard of verification: formal correctness proofs. A formal proof provides mathematical certainty that an algorithm satisfies its requirements under stated assumptions. No testing can provide this level of assurance—only proof.
This page introduces the techniques of formal proof for synchronization algorithms. We will cover:
By the end, you will understand how to construct and verify rigorous proofs of synchronization correctness.
By the end of this page, you will understand the structure of formal correctness proofs, master invariant-based proof techniques for mutual exclusion, apply variant-based proofs for progress and bounded waiting, and be able to construct complete correctness proofs for synchronization algorithms.
Formal proofs of concurrent algorithms differ from sequential program proofs. The non-determinism introduced by arbitrary scheduling requires techniques that account for all possible interleavings.
We model a concurrent program as a transition system:
States (S): The set of all possible configurations of shared and local variables.
Initial States (S₀ ⊆ S): The starting configurations.
Transitions (→ ⊆ S × S): The set of atomic steps that take the system from one state to another.
A program execution is a sequence of states s₀ → s₁ → s₂ → ... where s₀ ∈ S₀ and each sᵢ → sᵢ₊₁ is a valid transition.
Properties of concurrent systems divide into two categories:
Safety Properties: "Nothing bad ever happens"
Liveness Properties: "Something good eventually happens"
| Property Type | Intuition | Examples | Proof Technique |
|---|---|---|---|
| Safety | Nothing bad happens | Mutual exclusion, deadlock-freedom | Invariants |
| Liveness | Something good happens | Progress, bounded waiting, termination | Variants + fairness |
Liveness properties require fairness assumptions about the scheduler. Without them, a scheduler could indefinitely delay any process, violating any liveness property trivially.
Weak Fairness: If a process is continuously enabled (can take a step), it eventually takes a step.
Strong Fairness: If a process is infinitely often enabled (can take a step infinitely often), it eventually takes a step.
Most liveness proofs assume at least weak fairness. Without fairness, we can only prove safety properties.
Consider a spinlock: a process spins in 'while(locked){}'. With weak fairness, if the lock is eventually released, the process will eventually see it and proceed. Without fairness, the scheduler could always schedule other processes, and the spinner never gets a chance to check.
The primary technique for proving safety properties is invariant-based reasoning. An invariant is a predicate over states that, once established, remains true throughout execution.
An invariant I is inductive if:
Initialization: I holds in all initial states
∀s ∈ S₀: I(s)
Preservation: I is preserved by all transitions
∀s, s': (I(s) ∧ s → s') → I(s')
If I is inductive, then I holds in all reachable states. This follows by induction on the length of executions.
To prove mutual exclusion:
Define an invariant I that:
I(s) → at_most_one_in_cs(s)Since I holds in all reachable states, mutual exclusion holds in all reachable states.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
// Structure of an Invariant-Based Proof // Goal: Prove mutual exclusion for algorithm A // STEP 1: Define the invariant I// I must be strong enough to imply mutual exclusion// I must be preserved by every atomic action in the algorithm Example invariant for a lock-based algorithm: I ≡ (in_cs[i] → lock == HELD_BY_i) for all i // STEP 2: Prove initialization// Show I holds in the initial stateProof: Initially, no process is in CS. in_cs[i] = false for all i. Therefore, (in_cs[i] → lock == HELD_BY_i) is vacuously true. ∴ I holds initially. ✓ // STEP 3: Prove preservation// For each atomic action a, show: I(s) ∧ s →ᵃ s' → I(s') Action: Process i executes "acquire lock" Precondition: I(s), lock is free Effect: lock = HELD_BY_i Goal: Show I(s') Proof: No process was in CS (since lock was free and I(s)). After: if i enters CS, in_cs[i] = true, lock = HELD_BY_i. (in_cs[i] → lock == HELD_BY_i) holds. ✓ Action: Process i executes "release lock" Precondition: I(s), in_cs[i] = true Effect: lock = FREE, in_cs[i] = false Goal: Show I(s') Proof: After, in_cs[i] = false. (false → lock == HELD_BY_i) is true. ✓ [Continue for all actions...] // STEP 4: Conclude mutual exclusion// Show I → MUTEX where MUTEX ≡ ¬(in_cs[i] ∧ in_cs[j]) for i ≠ j Proof: Suppose in_cs[i] ∧ in_cs[j] for some i ≠ j. By I: lock == HELD_BY_i (since in_cs[i]) By I: lock == HELD_BY_j (since in_cs[j]) But HELD_BY_i ≠ HELD_BY_j, contradiction. ∴ ¬(in_cs[i] ∧ in_cs[j]). MUTEX holds. ✓Sometimes the obvious invariant isn't strong enough to be inductive. We may need to strengthen it—add more conjuncts that help preservation even if they're not directly needed for the final property.
For example, if we need to prove P, and P alone isn't inductive, we find Q such that:
P ∧ Q is inductiveP ∧ Q → P (trivially true)Finding the right strengthening is an art. Common techniques:
When a simple invariant fails to be preserved by some transition, ask: 'What additional condition would make it preserved?' The answer often reveals the strengthening needed. Invariant discovery is iterative—propose, test preservation, refine.
Let us construct a complete, rigorous proof that Peterson's algorithm satisfies mutual exclusion.
12345678910111213141516171819202122
// Peterson's Algorithm - Formal Specification // Shared Variables:// flag[0], flag[1] : Boolean (initially false)// turn : {0, 1} (initially 0) // Local Variables for process i:// pc[i] : {idle, want, wait, cs, exit} (program counter) // Process i (for i ∈ {0, 1}, let j = 1 - i): // Location transitions:// idle → want: flag[i] := true// want → wait: turn := j// wait → cs: (flag[j] = false) ∨ (turn = i) [guard]// cs → exit: <critical section complete>// exit → idle: flag[i] := false // Initial State:// pc[0] = idle, pc[1] = idle// flag[0] = false, flag[1] = false// turn = 0We propose the following invariant:
I ≡ ∀i ∈ {0,1}: (pc[i] ∈ {wait, cs, exit} → flag[i] = true)
∧
∀i ∈ {0,1}: (pc[i] = cs → (flag[j] = false ∨ turn = i))
The first conjunct says: if a process is waiting, in CS, or exiting, its flag is set.
The second conjunct says: if a process is in CS, either the other's flag is false or it's this process's turn.
123456789101112131415161718192021222324
// Proof: I holds in the initial state Initial state: pc[0] = idle, pc[1] = idle flag[0] = false, flag[1] = false turn = 0 First conjunct for i = 0: pc[0] ∈ {wait, cs, exit}? No (pc[0] = idle) Therefore, (pc[0] ∈ {wait, cs, exit} → flag[0] = true) is vacuously true. ✓ First conjunct for i = 1: pc[1] ∈ {wait, cs, exit}? No (pc[1] = idle) Vacuously true. ✓ Second conjunct for i = 0: pc[0] = cs? No (pc[0] = idle) Vacuously true. ✓ Second conjunct for i = 1: pc[1] = cs? No (pc[1] = idle) Vacuously true. ✓ ∴ I holds in the initial state. ✓We must show that each transition preserves I. There are 5 transitions per process; we analyze the critical ones:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879
// Proof: each transition preserves I // TRANSITION: idle → want (for process i)// Effect: flag[i] := true, pc[i] := want Before: Assume I holds.After: - First conjunct for i: pc[i] = want ∉ {wait, cs, exit}, so vacuously true. ✓ - First conjunct for j: unchanged (we only modified process i's state). ✓ - Second conjuncts: pc[i] ≠ cs, pc[j] unchanged. If pc[j] was not cs, still not cs. If pc[j] = cs, then by I before, (flag[i] = false ∨ turn = j). But we just set flag[i] = true! Does this break I? Hmm, potential issue. Let's check: If pc[j] = cs before, we had (flag[i] = false ∨ turn = j). After, flag[i] = true, so we need turn = j to maintain. But wait—we only set OUR flag. Turn was set during j's want→wait. Since j is in cs, j already passed the guard, so turn = j at that time. Turn only changes in want→wait transitions. Since we're in idle→want, we don't change turn. ∴ turn = j still holds. ✓ // TRANSITION: want → wait (for process i)// Effect: turn := j, pc[i] := wait Before: Assume I holds.After: - First conjunct for i: pc[i] = wait, flag[i] was set in idle→want, and we haven't cleared it. But wait, prove that flag[i] = true. Issue: We need flag[i] = true when pc[i] = wait. Strengthen I: Add "pc[i] ∈ {want, wait, cs, exit} → flag[i] = true" (extend the set to include 'want') OK, assuming strengthened I: Before, pc[i] = want, so flag[i] = true. We don't modify flag[i] in this transition. After, pc[i] = wait, flag[i] = true. ✓ - First conjunct for j: unchanged. ✓ - Second conjunct for i: pc[i] = wait ≠ cs, vacuously true. ✓ - Second conjunct for j: If pc[j] = cs, need (flag[i] = false ∨ turn = i). We just set turn = j, so turn ≠ i (since i ≠ j). We need flag[i] = false. But flag[i] = true! This seems like a problem... but wait: If pc[j] = cs, then j passed the guard: flag[i] = false ∨ turn = j. Since we're now setting turn = j, that's consistent. After this transition, turn = j, so (turn = j) is true. ∴ (flag[i] = false ∨ turn = j) holds. But the invariant for j says (flag[i] = false ∨ turn = j). After, turn = j, so this holds. ✓ // TRANSITION: wait → cs (for process i)// Guard: (flag[j] = false) ∨ (turn = i)// Effect: pc[i] := cs Before: Assume I holds, and guard is true.After: - First conjunct for i: pc[i] = cs, flag[i] was true (strengthened I). ✓ - Second conjunct for i: pc[i] = cs, need (flag[j] = false ∨ turn = i). But this was exactly the guard! We only take this transition when the guard is true. ∴ The condition holds. ✓ // TRANSITION: exit → idle (for process i) // Effect: flag[i] := false, pc[i] := idle Before: Assume I holds.After: - First conjunct for i: pc[i] = idle ∉ {want, wait, cs, exit}. Vacuous. ✓ - We set flag[i] = false. Does this affect second conjunct for j? If pc[j] = cs, we need (flag[i] = false ∨ turn = j). After, flag[i] = false, so this holds! ✓ ∴ All transitions preserve I. ✓1234567891011121314151617181920212223242526272829
// Proof: I → MUTEX // MUTEX ≡ ¬(pc[0] = cs ∧ pc[1] = cs) Proof by contradiction: Assume I holds and pc[0] = cs ∧ pc[1] = cs. By second conjunct of I applied to process 0: pc[0] = cs → (flag[1] = false ∨ turn = 0) ∴ flag[1] = false ∨ turn = 0 ... (1) By second conjunct of I applied to process 1: pc[1] = cs → (flag[0] = false ∨ turn = 1) ∴ flag[0] = false ∨ turn = 1 ... (2) By first conjunct of I applied to process 0: pc[0] = cs → flag[0] = true ∴ flag[0] = true ... (3) By first conjunct of I applied to process 1: pc[1] = cs → flag[1] = true ∴ flag[1] = true ... (4) From (1) and (4): Since flag[1] = true, we must have turn = 0. From (2) and (3): Since flag[0] = true, we must have turn = 1. But turn cannot be both 0 and 1. Contradiction! ∴ ¬(pc[0] = cs ∧ pc[1] = cs). MUTEX holds. ∎We have rigorously proven that Peterson's algorithm satisfies mutual exclusion. The key insight is that the turn variable creates an asymmetry: both processes cannot have 'their turn' simultaneously, so both cannot satisfy the entry guard at the same time.
Liveness properties assert that something good eventually happens. We cannot prove such properties using invariants alone—invariants only constrain what CAN happen, not what WILL happen.
To prove liveness, we use variant functions (also called ranking functions or progress measures):
A variant is a function V: States → W where:
W is a well-founded set (no infinite descending chains)VV cannot decrease foreverCommon choices for W:
To prove progress (if CS is empty and processes contend, one enters):
12345678910111213141516171819202122232425262728293031323334353637383940414243
// Structure of a Variant-Based Liveness Proof // Goal: Prove that waiting processes eventually enter CS // STEP 1: Define the progress condition// C(s) ≡ "processes are waiting but CS is empty" // STEP 2: Define the variant V// V: States → ℕ (or some well-founded set)// V measures "how far" we are from a process entering CS Example for Peterson's: V(s) = (2 if both in wait, 1 if one in wait and one not, 0 otherwise) But this is too coarse. Better: V(s) = < number of processes ahead in "queue", number of steps until guard satisfied > // STEP 3: Show V decreases while C holds// Under fairness, show that eventually:// Either C becomes false (a process enters - goal achieved!)// Or V decreases Proof sketch: Case: Both processes in wait state, turn = j. Then process i with turn = i will satisfy guard (flag[j] ∨ turn = i). Since turn = j (for j), process j is blocked. But process i's guard (flag[j] ∨ turn = i) evaluates to (true ∨ false) = true. Wait, let me reconsider... If turn = j, then: - Process i's guard: flag[j] = true (j is waiting), turn = i = (1-j) ≠ j. So flag[j] = true and turn ≠ i. Guard is false for i. - Process j's guard: flag[i] = true (i is waiting), turn = j. So turn = j. Guard (flag[i]=false ∨ turn=j) becomes (false ∨ true) = true! So process j enters. V decreases (one fewer waiting). ✓ // STEP 4: Conclude// Since W is well-founded and V decreases while C holds,// eventually C becomes false (some process in CS).// ∴ Progress holds.Bounded waiting requires a stronger argument: not just that SOME process enters, but that EACH waiting process enters within bounded bypasses.
We use a variant that tracks the specific waiting process:
Vᵢ(s) = "distance for process i to enter CS"
And show that:
Liveness proofs fundamentally require fairness assumptions. The variant decreases when the 'right' process takes a step. Fairness guarantees that the 'right' process eventually takes its step. Without fairness, we cannot conclude that the variant ever decreases.
Let us complete the correctness proof for Peterson's algorithm by proving progress and bounded waiting.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
// Proof: Peterson's algorithm satisfies Progress // Goal: If CS is empty and at least one process is in entry (want or wait),// eventually some process enters CS. // Assumption: Weak fairness (continuously enabled → eventually executes) // Case 1: One process (say i) is in entry, the other (j) is in remainder. Process i is at want or wait. j is in remainder (idle state), so flag[j] = false. If i is at want: Under fairness, i eventually executes want → wait (sets turn = j). Now i is at wait. i is at wait: Guard: flag[j] = false ∨ turn = i Since flag[j] = false (j in remainder), guard is TRUE. Under fairness, i eventually executes wait → cs. ∴ Process i enters CS. Progress satisfied. ✓ // Case 2: Both processes are in entry. Both at want or wait states. Sub-case 2a: At least one is at want. Under fairness, it executes want → wait. Eventually, both are at wait. Sub-case 2b: Both at wait. flag[0] = true, flag[1] = true (by invariant). turn = 0 or turn = 1. If turn = 0: Process 0's guard: flag[1] = true, turn = 0. Guard = (true ∨ true) = true... wait, that's wrong. Guard = (flag[1] = false) ∨ (turn = 0) = false ∨ true = true. ✓ Process 0 can proceed. Under fairness, process 0 enters CS. If turn = 1: By symmetric argument, process 1 enters CS. ∴ Some process enters CS. Progress satisfied. ✓ // Conclusion: Under weak fairness, Peterson's algorithm has Progress. ∎1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768
// Proof: Peterson's algorithm satisfies Bounded Waiting with B = 1 // Goal: After process i enters its entry section, process j can enter// CS at most once before process i enters. // Assume process i enters entry section at time t₀ (sets flag[i] = true). // Case 1: Process j is not in entry/CS at time t₀. j is in remainder (idle), so flag[j] = false. As shown in progress proof, i will enter CS. j bypasses i 0 times. ✓ // Case 2: Process j is in CS at time t₀. j will eventually exit (finite CS execution assumption). When j exits: - j sets turn = i (in exit section) - j sets flag[j] = false Now, if j wants to enter again: j sets flag[j] = true j sets turn = i (in want → wait) j arrives at wait with turn = i In the meantime, i is in entry (want or wait). If i is at wait: turn = i (set by j's exit or j's entry!) i's guard: flag[j] = true, turn = i Guard = (true ∨ true) = true... no wait, Guard = (flag[j] = false) ∨ (turn = i) = false ∨ true = TRUE. ✓ i can enter. If i is at want: Under fairness, i moves to wait. i sets turn = j. But then j's exit will set turn = i. Eventually i sees turn = i and enters. Key insight: After j exits CS and before i enters CS, if j sets turn again (in want→wait), it sets turn = i. This guarantees i's guard becomes true. j entered CS once (was already in at t₀), i then enters. j bypasses i at most 1 time. ✓ // Case 3: Process j is in entry at time t₀. j is at want or wait. turn is either 0 or 1. If turn = j (i.e., it's j's turn): j can enter (guard satisfied). j enters CS (bypass 1). j exits, sets turn = i. Now i's guard: (flag[j] = false) ∨ (turn = i). After j's exit, flag[j] = false, so guard is true. i enters. j bypassed i once. ✓ If turn = i: j's guard: flag[i] = true, turn = i ≠ j. Guard false. j cannot enter. i's guard: flag[j] = true, turn = i. Guard true. i enters. j bypassed i zero times. ✓ // Conclusion: In all cases, j enters at most once before i.// Bounded Waiting holds with B = 1. ∎We have now rigorously proven that Peterson's algorithm satisfies all three requirements: Mutual Exclusion (via invariant proof), Progress (via case analysis with fairness), and Bounded Waiting with B=1 (via bypass counting). Peterson's algorithm is correct!
Let us consolidate the proof techniques we have developed:
| Property | Type | Technique | Key Elements |
|---|---|---|---|
| Mutual Exclusion | Safety | Invariant | Find I such that I is inductive and I → MUTEX |
| Absence of Deadlock | Safety | Invariant | Show ¬(all waiting ∧ none can proceed) |
| Progress | Liveness | Variant + Fairness | Show variant decreases until entry |
| Bounded Waiting | Liveness | Bypass Counting | Count max bypasses per waiting process |
| Termination | Liveness | Variant | Show execution eventually completes |
Certain invariant patterns recur across synchronization algorithms:
at(L) → var = val (e.g., in CS implies lock held)in_cs[i] → gate_condition[i] where gate prevents simultaneous entryturn has one value at a time, so two different turn values cannot holdflag[i] = true ↔ process i is interestedΣ in_cs[i] + Σ waiting[i] + Σ idle[i] = n (all processes accounted for)| Pitfall | Description | Remedy |
|---|---|---|
| Weak invariant | Invariant doesn't imply property | Strengthen with additional conjuncts |
| Non-inductive | Invariant not preserved by all transitions | Strengthen or relax invariant |
| Missed transition | Forgot to check some program action | Systematically enumerate all actions |
| Conflated atomicity | Treated non-atomic as atomic | Decompose into true atomic steps |
| Omitted fairness | Proved liveness without fairness | State fairness assumption explicitly |
| Case explosion | Didn't use structure to simplify | Use symmetry and case merging |
While hand-written proofs are valuable for understanding, they are error-prone. Mechanized proofs use computer assistance to verify correctness with mathematical certainty.
Provers like Coq, Isabelle/HOL, and Lean allow humans to construct proofs interactively, with the computer verifying each step:
Tools like SPIN, TLA+, and UPPAAL exhaustively explore the state space:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546
--------------------------- MODULE Peterson ---------------------------EXTENDS Integers VARIABLES flag, turn, pc vars == <<flag, turn, pc>> Init == /\ flag = [i \in {0, 1} |-> FALSE] /\ turn \in {0, 1} /\ pc = [i \in {0, 1} |-> "idle"] (* Process i transitions *)Enter(i) == /\ pc[i] = "idle" /\ pc' = [pc EXCEPT ![i] = "want"] /\ flag' = [flag EXCEPT ![i] = TRUE] /\ UNCHANGED turn SetTurn(i) == LET j == 1 - i IN /\ pc[i] = "want" /\ pc' = [pc EXCEPT ![i] = "wait"] /\ turn' = j /\ UNCHANGED flag EnterCS(i) == LET j == 1 - i IN /\ pc[i] = "wait" /\ (flag[j] = FALSE \/ turn = i) /\ pc' = [pc EXCEPT ![i] = "cs"] /\ UNCHANGED <<flag, turn>> ExitCS(i) == LET j == 1 - i IN /\ pc[i] = "cs" /\ pc' = [pc EXCEPT ![i] = "idle"] /\ flag' = [flag EXCEPT ![i] = FALSE] /\ turn' = j Next == \E i \in {0, 1}: Enter(i) \/ SetTurn(i) \/ EnterCS(i) \/ ExitCS(i) (* Mutual exclusion property *)MutualExclusion == ~(pc[0] = "cs" /\ pc[1] = "cs") (* Specification *)Spec == Init /\ [][Next]_vars /\ WF_vars(Next) THEOREM Spec => []MutualExclusion =========================================================================Formal verification is used in high-stakes domains:
For synchronization specifically, libraries like liblfds (lock-free data structures) have undergone formal verification.
Formal verification is most valuable when: (1) Correctness is critical (safety, security, financial), (2) Testing is insufficient (concurrency, rare conditions), (3) The system will run for long periods without restart, (4) Bugs have severe consequences (data loss, safety hazards). For synchronization algorithms—especially in OS kernels—formal verification is increasingly standard.
We have explored the rigorous mathematical foundations of correctness proofs for synchronization algorithms. Let us consolidate the key concepts.
With this page, we have completed Module 3: Requirements for Solutions. You now possess:
These foundations prepare you for studying specific algorithms (Peterson's, Dekker's) in depth, and for understanding the hardware and OS mechanisms (atomic operations, locks, semaphores) that implement synchronization in practice.
Next chapters will explore:
Congratulations! You have mastered the requirements for solutions to the critical section problem—mutual exclusion, progress, and bounded waiting—along with systematic evaluation techniques and formal proof methods. This foundational knowledge will serve you throughout your study of operating systems and concurrent programming.