Featured image of post Why Everything Stops When Everyone Waits: The Mathematical Theory of Deadlocks, Resource Allocation Graphs, Dijkstra's Banker's Algorithm, and Livelock

Why Everything Stops When Everyone Waits: The Mathematical Theory of Deadlocks, Resource Allocation Graphs, Dijkstra's Banker's Algorithm, and Livelock

A rigorous systems and mathematical treatise on concurrency deadlocks: Dijkstra's Dining Philosophers, Coffman's four necessary conditions (1971), Holt's Resource Allocation Graph (RAG) cycle theorems for single- and multi-instance resources, a formal proof of Total Lock Ordering, a complete 5-process matrix trace of Dijkstra's Banker's Algorithm, RDBMS Wait-For Graph (WFG) detection, distributed Wait-Die vs. Wound-Wait protocols, the Ostrich Algorithm, and Livelock vs. Starvation.

1. Introduction: Eternal Paralysis Born from Obedience to Local Rules

An oft-cited piece of early twentieth-century railway lore recalls a bill reportedly introduced in the Kansas State Legislature to prevent collisions at rail-line intersections:

“When two trains approach each other at a crossing, both shall come to a full stop, and neither shall start up again until the other has passed."

If two locomotive engineers faithfully obey this statute upon arriving at an intersection simultaneously, the outcome is immediate and permanent: Train A waits for Train B to clear the crossing, while Train B waits for Train A to clear the crossing. Neither engine suffers a mechanical breakdown, neither engineer is asleep at the throttle, and yet both trains remain frozen on the tracks forever.

In concurrent computing—spanning operating system kernels, multi-threaded C++/Rust/Java applications, relational database management systems (RDBMS), and globally distributed transaction engines—this pathology is known as Deadlock. Unlike a segmentation fault or an arithmetic overflow, a deadlock is not caused by an invalid instruction; it emerges when multiple autonomous threads or transactions obey locally sensible resource-acquisition rules whose global interaction forms an inescapable cycle of mutual waiting.

1.1 Dijkstra’s Dining Philosophers Problem (1965)

In 1965, the Dutch computer scientist Edsger W. Dijkstra formulated an examination problem concerning five competing processes sharing five magnetic tape drives, which C. A. R. Hoare subsequently cast into its immortal allegorical form: The Dining Philosophers Problem.

Five philosophers, $P_0, P_1, P_2, P_3, P_4$, sit around a circular table. In front of each philosopher is a plate of tangled spaghetti, and between each adjacent pair of plates lies a single fork—five forks in total, denoted $F_0, F_1, F_2, F_3, F_4$. Philosopher $P_i$ ($i \in \{0, 1, 2, 3, 4\}$) has fork $F_i$ to their left and fork $F_{(i+1)\bmod 5}$ to their right.

Each philosopher alternates between two states: Thinking and Eating. Eating requires simultaneous exclusive possession of both the left fork $F_i$ and the right fork $F_{(i+1)\bmod 5}$. A fork can be held by only one philosopher at a time, and no philosopher may snatch a fork from a neighbor’s hand. Suppose every philosopher executes the following natural, symmetric protocol:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// Symmetric (and fatal) protocol for Philosopher P_i (i = 0, 1, 2, 3, 4)
void philosopher(int i) {
    int left  = i;
    int right = (i + 1) % 5;
    while (true) {
        think();                     // 1. Meditate independently
        acquire_fork(left);          // 2. Pick up left fork F_i (blocks if held)
        acquire_fork(right);         // 3. Pick up right fork F_{(i+1) mod 5} (blocks if held)
        eat();                       // 4. Eat spaghetti using both forks
        release_fork(right);         // 5. Put down right fork
        release_fork(left);          // 6. Put down left fork
    }
}

In testing, this program may run flawlessly for days if philosophers become hungry at staggered times. Yet if all five philosophers happen to become hungry at the exact same microsecond and each picks up their left fork $F_i$ simultaneously:

  • $P_0$ holds $F_0$ and blocks waiting for $F_1$ (held by $P_1$).
  • $P_1$ holds $F_1$ and blocks waiting for $F_2$ (held by $P_2$).
  • $P_2$ holds $F_2$ and blocks waiting for $F_3$ (held by $P_3$).
  • $P_3$ holds $F_3$ and blocks waiting for $F_4$ (held by $P_4$).
  • $P_4$ holds $F_4$ and blocks waiting for $F_0$ (held by $P_0$).

All five forks are held; zero forks remain on the table; every philosopher is blocked waiting for a fork that will only be released after its holder finishes eating. Progress stops forever.

1.2 Formal Definition of Deadlock

Let $\mathcal{P} = \{P_1, P_2, \dots, P_n\}$ denote the set of all processes (threads or transactions) in a system.

Definition (Deadlocked Set):
A non-empty subset of processes $D \subseteq \mathcal{P}$ ($D \neq \emptyset$) is in a deadlock state if and only if every process $P_i \in D$ is permanently blocked waiting for an event (such as the release of a resource or receipt of a signal) that can only be triggered by another process $P_j \in D$ belonging to the exact same waiting set.

Because every member of $D$ is asleep, no member of $D$ can ever execute an instruction to trigger the waking event. Without external intervention, the processes in $D$ remain blocked for eternity.


2. Coffman’s Four Conditions (1971): The Anatomy of Resource Deadlock

In their classic 1971 ACM Computing Surveys paper, “System Deadlocks,” Edward G. Coffman Jr., M. J. Elphick, and Arie Shoshani proved that a resource deadlock can occur only if four necessary conditions hold simultaneously in the system:

  1. Mutual Exclusion:
    At least one resource type must be held in a non-shareable (exclusive) mode. Only one process may use the resource instance at any given instant; any other process requesting that instance must be delayed until the holder releases it.
  2. Hold and Wait:
    A process must be currently holding at least one resource while simultaneously waiting to acquire additional resources that are currently being held exclusively by other processes.
  3. No Preemption:
    Resources cannot be forcibly stripped away from a process by the operating system or by another process; a resource can be released only voluntarily by the process holding it, after that process has completed its task.
  4. Circular Wait:
    There must exist a closed directed chain of waiting processes $\{P_0, P_1, \dots, P_{k-1}\}$ ($k \ge 2$) such that for every $i \in \{0, 1, \dots, k-1\}$, process $P_i$ is waiting for a resource currently held by process $P_{(i+1)\bmod k}$ (i.e., $P_0$ waits for $P_1$, $P_1$ waits for $P_2$, $\dots$, and $P_{k-1}$ waits for $P_0$).

Architectural Insight: The first three conditions (Mutual Exclusion, Hold and Wait, No Preemption) are static policy rules governing how resources behave, whereas the fourth condition (Circular Wait) is a dynamic runtime state that arises when specific request timings align under those policies. Crucially, because all four conditions are individually necessary for deadlock, logically negating even a single one of the four Coffman conditions makes deadlock mathematically impossible ($0\%$ probability).


3. Holt’s Resource Allocation Graph (RAG) and the Cycle Theorems (1972)

In 1972, Richard C. Holt formalized the relationship between Coffman’s conditions and graph topology by introducing the Resource Allocation Graph (RAG).

3.1 Formal Bipartite Directed Graph Definition

At any instant $t$, the state of resource requests and allocations is modeled as a directed bipartite graph $G = (V, E)$:

  • Vertex Set $V = \mathcal{P} \cup \mathcal{R}$ ($\mathcal{P} \cap \mathcal{R} = \emptyset$):
    • $\mathcal{P} = \{P_1, P_2, \dots, P_n\}$ is the set of Process Vertices (drawn as circles $\bigcirc$).
    • $\mathcal{R} = \{R_1, R_2, \dots, R_m\}$ is the set of Resource Type Vertices (drawn as rectangles $\square$). Each resource type $R_j$ possesses $W_j = |R_j| \ge 1$ identical, interchangeable instances (drawn as $W_j$ black dots $\bullet$ inside rectangle $R_j$). A mutex lock has $W_j = 1$ (single-instance), whereas a pool of identical database connections or memory frames has $W_j > 1$ (multi-instance).
  • Directed Edge Set $E = E_{\text{req}} \cup E_{\text{alloc}}$:
    • Request Edge $P_i \to R_j \in E_{\text{req}}$: Directed from process $P_i$ to resource type $R_j$, indicating that $P_i$ is currently blocked waiting to acquire one instance of $R_j$.
    • Allocation (Assignment) Edge $R_j \to P_i \in E_{\text{alloc}}$: Directed from a specific instance dot $\bullet$ inside $R_j$ to process $P_i$, indicating that $P_i$ currently holds one allocated instance of $R_j$. Conservation of instances requires that the out-degree $d^+(R_j)$ never exceed $W_j$: $$\forall R_j \in \mathcal{R}: \quad d^+(R_j) = \bigl|\{(R_j \to P_i) \in E_{\text{alloc}}\}\bigr| \le W_j$$

3.2 The Three Fundamental RAG Cycle Theorems

How does the presence of a directed cycle in $G = (V, E)$ relate to deadlock? Holt proved three theorems that distinguish single-instance systems from multi-instance systems:

Theorem 1 (Acyclicity Implies Freedom from Deadlock):
If a Resource Allocation Graph $G$ contains no directed cycles (i.e., $G$ is a Directed Acyclic Graph, DAG), then no process in the system is deadlocked. Equivalently (by contraposition), the existence of at least one directed cycle in $G$ is a necessary condition for deadlock.

Theorem 2 (Single-Instance Necessary and Sufficient Cycle Theorem):
If every resource type in the system has exactly one instance ($\forall j \in \{1, \dots, m\}, \; W_j = 1$), then a directed cycle in $G$ is both a necessary and sufficient condition for deadlock. Every process lying on a directed cycle is deadlocked.

Theorem 3 (Multi-Instance Cycle Non-Sufficiency Theorem):
If at least one resource type has multiple instances ($\exists j, \; W_j > 1$), then a directed cycle in $G$ is a necessary condition, but NOT a sufficient condition, for deadlock. A graph $G$ can contain a directed cycle even when zero processes are deadlocked!

3.3 Concrete Counterexample: A Cyclic RAG That Is NOT Deadlocked

To see why Theorem 3 holds, consider a system with two multi-instance resource types $R_1$ ($W_1 = 2$ instances) and $R_2$ ($W_2 = 2$ instances), and four processes $\{P_1, P_2, P_3, P_4\}$:

  • $R_1$’s two instances are currently allocated to $P_2$ and $P_3$ ($R_1 \to P_2$ and $R_1 \to P_3$).
  • $R_2$’s two instances are currently allocated to $P_1$ and $P_4$ ($R_2 \to P_1$ and $R_2 \to P_4$).
  • $P_1$ requests an instance of $R_1$ ($P_1 \to R_1$), and $P_3$ requests an instance of $R_2$ ($P_3 \to R_2$).
  • Crucially, $P_2$ and $P_4$ are not requesting any additional resources ($d^+(P_2) = d^+(P_4) = 0$).
  flowchart TD
    subgraph RAG_Single["Case A: Single-Instance RAG (Cycle <==> Deadlock)"]
        PA1(("P1 (Blocked)")) -->|"Requests"| RA1["R1 (1 instance: ●)"]
        RA1 -->|"Allocated to"| PA2(("P2 (Blocked)"))
        PA2 -->|"Requests"| RA2["R2 (1 instance: ●)"]
        RA2 -->|"Allocated to"| PA1
    end

    subgraph RAG_Multi["Case B: Multi-Instance RAG (Cycle Exists, Yet ZERO Deadlock!)"]
        PB1(("P1 (Blocked)")) -->|"Requests"| RB1["R1 (2 instances: ●●)"]
        RB1 -->|"Allocated #1"| PB3(("P3 (Blocked)"))
        PB3 -->|"Requests"| RB2["R2 (2 instances: ●●)"]
        RB2 -->|"Allocated #1"| PB1
        RB1 -->|"Allocated #2"| PB2(("P2 (RUNNING: No Requests!)"))
        RB2 -->|"Allocated #2"| PB4(("P4 (RUNNING: No Requests!)"))
    end

Inspect Case B carefully. The graph clearly contains the directed cycle:

$$P_1 \to R_1 \to P_3 \to R_2 \to P_1$$

Yet this system is NOT deadlocked! Because process $P_4$ has out-degree $d^+(P_4) = 0$ (a sink vertex), it is actively running on the CPU and will finish its work in finite time, releasing its instance of $R_2$ (deleting edge $R_2 \to P_4$). That freed instance of $R_2$ is immediately granted to waiting process $P_3$ (converting $P_3 \to R_2$ into $R_2 \to P_3$), which breaks the cycle and allows $P_3$ and then $P_1$ to run to completion! In multi-instance systems, exact deadlock detection requires Graph Reduction (iteratively removing unblocked sink processes and their incident edges) or verifying the existence of an inescapable Knot.


4. Strategy 1: Deadlock Prevention and the Mathematical Proof of Total Lock Ordering

The first engineering strategy against deadlock is Deadlock Prevention: designing the system’s resource acquisition rules so that at least one of Coffman’s four conditions can never hold.

4.1 Attacking Conditions 1, 2, and 3 (and Their Practical Limits)

  1. Negating Mutual Exclusion (Condition 1):
    Using immutable data structures, Read-Copy-Update (RCU in the Linux kernel, where readers execute with zero locks), or hardware atomic primitives (CAS / LL-SC lock-free algorithms) eliminates mutual exclusion altogether. However, resources that inherently require serialized mutation cannot avoid mutual exclusion.
  2. Negating Hold and Wait (Condition 2):
    Require a thread to either acquire all required locks atomically up front before entering a computation, or release all currently held locks before requesting a new one. In practice, this is rarely viable because a complex function often cannot know which locks it will need until it inspects runtime data, and holding locks for long durations causes severe resource underutilization and starvation.
  3. Negating No Preemption (Condition 3):
    If a thread holding lock $L_1$ attempts to acquire $L_2$ via non-blocking pthread_mutex_trylock(&L2) and fails, it immediately unlocks $L_1$ and backs off. While useful in specific cases, aborting midway through mutating a data structure leaves shared state corrupted unless full transactional rollback logs are maintained, and naive retry loops trigger Livelock (Section 8).

4.2 Negating Circular Wait (Condition 4): Total Lock Ordering (Lock Hierarchy)

In production systems programming (OS kernels, storage engines, and multi-threaded runtimes), the universal gold standard for deadlock prevention is negating Condition 4 via Total Lock Ordering.

Let $\mathcal{L} = \{L_1, L_2, \dots, L_m\}$ be the universe of all mutex locks in the system. Define a strict global ranking function $\phi: \mathcal{L} \to \mathbb{N}$ that assigns a unique integer rank (or memory address order) to every lock:

$$\forall L_{\text{held}} \in \mathcal{H}: \quad \phi(L_{\text{held}}) < \phi(L_{\text{new}})$$

Let us prove by contradiction that this single rule makes Circular Wait mathematically impossible.

Theorem 4 (Impossibility of Circular Wait Under Total Lock Ordering):
If every thread in a system acquires locks strictly in increasing order of $\phi: \mathcal{L} \to \mathbb{N}$, then no circular wait cycle can ever form.

Proof by Contradiction:
Suppose, for the sake of contradiction, that a circular wait cycle of $k \ge 2$ threads $\{P_0, P_1, \dots, P_{k-1}\}$ forms at runtime. By the definition of circular wait, each thread $P_i$ ($i \in \{0, \dots, k-1\}$) currently holds some lock $L_i \in \mathcal{L}$ while blocked waiting to acquire lock $L_{(i+1)\bmod k} \in \mathcal{L}$, which is currently held by thread $P_{(i+1)\bmod k}$.

Because every thread $P_i$ obeys the Total Lock Ordering Rule when requesting $L_{(i+1)\bmod k}$ while holding $L_i$, the rank of the held lock must be strictly less than the rank of the requested lock for every $i \in \{0, 1, \dots, k-1\}$:

$$\phi(L_i) < \phi(L_{(i+1)\bmod k})$$

Chaining these $k$ strict inequalities around the cycle $i = 0, 1, \dots, k-1$ yields:

$$\phi(L_0) < \phi(L_1) < \phi(L_2) < \cdots < \phi(L_{k-1}) < \phi(L_0)$$

By the transitivity of the strict inequality relation $<$ on the natural numbers $\mathbb{N}$, this implies:

$$\phi(L_0) < \phi(L_0)$$

Since no number can be strictly less than itself, we have reached a logical contradiction ($\bot$). Therefore, no circular wait cycle can ever exist. $\blacksquare$

Eliminating AB-BA Deadlock in Bank Transfers & Dining Philosophers

Consider a classic concurrent bank transfer transfer(Account& from, Account& to, long amount). If Thread 1 calls transfer(A, B) (locking A then B) at the same instant Thread 2 calls transfer(B, A) (locking B then A), an AB-BA deadlock occurs. By ordering lock acquisitions by $\phi(\text{Account}) = \text{Account.id}$ (or via C++17 std::scoped_lock), the deadlock vanishes completely:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#include <mutex>

struct Account {
    unsigned long id;      // Unique global rank: φ(Account) = id
    long balance;
    std::mutex mtx;
};

void transfer_ordered(Account& from, Account& to, long amount) {
    if (&from == &to) return;

    // Enforce Total Lock Ordering: always lock the smaller ID first!
    Account* first  = (from.id < to.id) ? &from : &to;
    Account* second = (from.id < to.id) ? &to   : &from;

    std::lock_guard<std::mutex> lock1(first->mtx);
    std::lock_guard<std::mutex> lock2(second->mtx);

    from.balance -= amount;
    to.balance   += amount;
}

Similarly, in Dijkstra’s Dining Philosophers, numbering the forks $\phi(F_i) = i$ and requiring every philosopher to pick up their lower-numbered fork first means philosophers $P_0, P_1, P_2, P_3$ pick up left ($F_i$) then right ($F_{i+1}$), while philosopher $P_4$ alone breaks symmetry by picking up their right fork $F_0$ before their left fork $F_4$ ($0 < 4$). Deadlock becomes impossible.

In massive codebases like the Linux kernel (which contains tens of thousands of locks), humans cannot verify lock ordering by inspection alone. Since 2006, Linux has included lockdep, a runtime lock-dependency graph validator designed by Ingo Molnar. lockdep tracks lock classes as vertices and records a directed edge $A \to B$ whenever any thread acquires lock class $B$ while holding class $A$. If any thread ever attempts to acquire $B \to A$—even on a single-core machine where no actual deadlock occurred during the test—lockdep immediately detects the cycle in its dependency graph and prints a kernel warning splat with the exact offending stack traces!


5. Strategy 2: Deadlock Avoidance and Dijkstra’s Banker’s Algorithm

What if resources have multiple instances and processes acquire them in data-dependent orders that cannot be fixed statically? Deadlock Avoidance requires each process to declare upfront its Maximum Claim ($\mathbf{Max}$)—the maximum number of instances of each resource type it could ever need simultaneously. Before granting any dynamic request, the OS simulates the allocation and checks whether the resulting state remains a Safe State.

5.1 Safe States vs. Unsafe States

$$\forall k \in \{1, \dots, n\}: \quad \mathbf{Need}_{\pi(k)} \le \mathbf{Available} + \sum_{j=1}^{k-1} \mathbf{Allocation}_{\pi(j)}$$

A state that possesses no safe sequence is called Unsafe.

Crucial distinction:

$$\text{Deadlocked States} \subsetneq \text{Unsafe States}$$

An Unsafe State is NOT necessarily a Deadlocked State! Rather, an unsafe state is a state in which the OS has lost its guarantee: if the processes subsequently choose to request up to their declared maximums $\mathbf{Max}$ simultaneously, a deadlock will occur. Because the OS cannot gamble on processes behaving below their declared maximums, Dijkstra’s Banker’s Algorithm (1965, generalized to multiple resource types by A. N. Habermann in 1969) refuses to grant any request that would transition the system from a Safe State into an Unsafe State.

5.2 Data Structures and $O(m \cdot n^2)$ Complexity of the Banker’s Algorithm

For $n$ processes and $m$ resource types, the Banker’s Algorithm maintains four structures:

  1. $\mathbf{Available} \in \mathbb{N}_0^m$: Vector of currently unallocated instances of each resource type.
  2. $\mathbf{Max} \in \mathbb{N}_0^{n \times m}$: Matrix of maximum declared demand of each process $P_i$ for each resource $R_j$.
  3. $\mathbf{Allocation} \in \mathbb{N}_0^{n \times m}$: Matrix of instances currently allocated to each process $P_i$.
  4. $\mathbf{Need} \in \mathbb{N}_0^{n \times m}$: Matrix of remaining potential future requests, defined by the invariant: $$\mathbf{Need}[i, j] = \mathbf{Max}[i, j] - \mathbf{Allocation}[i, j]$$

When process $P_i$ issues a request vector $\mathbf{Request}_i \in \mathbb{N}_0^m$:

  1. Verify $\mathbf{Request}_i \le \mathbf{Need}_i$ (otherwise raise an error for exceeding maximum claim).
  2. Check $\mathbf{Request}_i \le \mathbf{Available}$ (if not, $P_i$ must wait until resources are free).
  3. Tentative Allocation (Pretend to grant): $$\mathbf{Available} \leftarrow \mathbf{Available} - \mathbf{Request}_i, \quad \mathbf{Allocation}_i \leftarrow \mathbf{Allocation}_i + \mathbf{Request}_i, \quad \mathbf{Need}_i \leftarrow \mathbf{Need}_i - \mathbf{Request}_i$$
  4. Run the Safety Algorithm: Initialize $\mathbf{Work} \leftarrow \mathbf{Available}$ and $\mathbf{Finish}[k] \leftarrow \text{false}$ for $k = 0 \dots n-1$. Repeatedly find an index $k$ such that $\mathbf{Finish}[k] == \text{false}$ and $\mathbf{Need}_k \le \mathbf{Work}$; when found, simulate $P_k$ finishing and returning its resources via $\mathbf{Work} \leftarrow \mathbf{Work} + \mathbf{Allocation}_k$ and $\mathbf{Finish}[k] \leftarrow \text{true}$.
    • If all $\mathbf{Finish}[k] == \text{true}$, the state is Safe $\implies$ commit the allocation!
    • Otherwise, the state is Unsafe $\implies$ roll back Step 3 and block $P_i$!

Because the outer loop runs at most $n$ times, scanning $n$ processes and comparing $m$-element vectors at each step, the Safety Algorithm runs in $O(m \cdot n^2)$ time.

5.3 Complete Step-by-Step Matrix Walkthrough (5 Processes $\times$ 3 Resources)

Consider a system with $n = 5$ processes $\{P_0, P_1, P_2, P_3, P_4\}$ and $m = 3$ resource types $(A, B, C)$ with total system instances:

$$\mathbf{Total} = (A = 10, \; B = 5, \; C = 7)$$

At snapshot $t_0$, the system state is given by:

Process$\mathbf{Allocation}$ $(A, B, C)$$\mathbf{Max}$ $(A, B, C)$$\mathbf{Need} = \mathbf{Max} - \mathbf{Allocation}$ $(A, B, C)$
$P_0$$(0, 1, 0)$$(7, 5, 3)$$(7, 4, 3)$
$P_1$$(2, 0, 0)$$(3, 2, 2)$$(1, 2, 2)$
$P_2$$(3, 0, 2)$$(9, 0, 2)$$(6, 0, 0)$
$P_3$$(2, 1, 1)$$(2, 2, 2)$$(0, 1, 1)$
$P_4$$(0, 0, 2)$$(4, 3, 3)$$(4, 3, 1)$
Sum Allocated$(7, 2, 5)$——

Subtracting the sum of allocated resources from $\mathbf{Total}$ yields the initial available vector:

$$\mathbf{Available} = (10, 5, 7) - (7, 2, 5) = \mathbf{(3, 3, 2)}$$

Trace 1: Proving Initial State $t_0$ Is Safe

Initialize $\mathbf{Work} = (3, 3, 2)$ and $\mathbf{Finish} = [\text{F}, \text{F}, \text{F}, \text{F}, \text{F}]$:

  1. Step 1: $P_0$ needs $(7,4,3) \not\le (3,3,2)$ (Wait). $P_1$ needs $(1,2,2) \le (3,3,2) \implies$ Select $P_1$! $P_1$ finishes and releases $\mathbf{Allocation}_1 = (2,0,0)$: $$\mathbf{Work} \leftarrow (3,3,2) + (2,0,0) = \mathbf{(5, 3, 2)}$$
  2. Step 2: $P_2$ needs $(6,0,0) \not\le (5,3,2)$ (Wait). $P_3$ needs $(0,1,1) \le (5,3,2) \implies$ Select $P_3$! $P_3$ finishes and releases $\mathbf{Allocation}_3 = (2,1,1)$: $$\mathbf{Work} \leftarrow (5,3,2) + (2,1,1) = \mathbf{(7, 4, 3)}$$
  3. Step 3: $P_4$ needs $(4,3,1) \le (7,4,3) \implies$ Select $P_4$! $P_4$ finishes and releases $\mathbf{Allocation}_4 = (0,0,2)$: $$\mathbf{Work} \leftarrow (7,4,3) + (0,0,2) = \mathbf{(7, 4, 5)}$$
  4. Step 4: Re-evaluating $P_0$, its need $(7,4,3) \le (7,4,5) \implies$ Select $P_0$! $P_0$ finishes and releases $\mathbf{Allocation}_0 = (0,1,0)$: $$\mathbf{Work} \leftarrow (7,4,5) + (0,1,0) = \mathbf{(7, 5, 5)}$$
  5. Step 5: Re-evaluating $P_2$, its need $(6,0,0) \le (7,5,5) \implies$ Select $P_2$! $P_2$ finishes and releases $\mathbf{Allocation}_2 = (3,0,2)$: $$\mathbf{Work} \leftarrow (7,5,5) + (3,0,2) = \mathbf{(10, 5, 7)} = \mathbf{Total}$$

Because the safe sequence $\langle P_1, P_3, P_4, P_0, P_2 \rangle$ exists, state $t_0$ is SAFE.

Trace 2: Granted Request vs. Rejected Unsafe Request

  • Request A — $P_1$ requests $\mathbf{Request}_1 = (1, 0, 2)$:
    Since $(1,0,2) \le \mathbf{Need}_1 = (1,2,2)$ and $(1,0,2) \le \mathbf{Available} = (3,3,2)$, tentative allocation leaves $\mathbf{Available} = (2, 3, 0)$, $\mathbf{Allocation}_1 = (3, 0, 2)$, and $\mathbf{Need}_1 = (0, 2, 0)$. Running the Safety Algorithm from $\mathbf{Work} = (2, 3, 0)$ immediately satisfies $\mathbf{Need}_1 = (0, 2, 0) \le (2, 3, 0)$, yielding the same safe sequence $\langle P_1, P_3, P_4, P_0, P_2 \rangle$. Request A is GRANTED immediately.
  • Request B — Immediately afterward (with $\mathbf{Available} = (2, 3, 0)$), $P_0$ requests $\mathbf{Request}_0 = (0, 2, 0)$:
    Even though $2$ units of $B$ are currently sitting idle in $\mathbf{Available} = (2, 3, 0)$, tentatively granting $(0, 2, 0)$ to $P_0$ reduces $\mathbf{Available}$ to $\mathbf{Work} = (2, 1, 0)$. Now inspect the remaining needs of all five processes against $\mathbf{Work} = (2, 1, 0)$:
    • $\mathbf{Need}_0 = (7, 2, 3) \not\le (2, 1, 0)$
    • $\mathbf{Need}_1 = (0, 2, 0) \not\le (2, 1, 0)$ (needs $2$ of $B$, only $1$ left!)
    • $\mathbf{Need}_2 = (6, 0, 0) \not\le (2, 1, 0)$
    • $\mathbf{Need}_3 = (0, 1, 1) \not\le (2, 1, 0)$ (needs $1$ of $C$, $0$ left!)
    • $\mathbf{Need}_4 = (4, 3, 1) \not\le (2, 1, 0)$ Zero processes can satisfy their $\mathbf{Need}$! The resulting state is UNSAFE, so the Banker’s Algorithm rolls back the tentative allocation and blocks $P_0$, even though $\mathbf{Available}$ physically had enough units of $B$!

6. Strategy 3: Deadlock Detection & Recovery (Wait-For Graphs, Wait-Die, and Wound-Wait)

Why don’t general-purpose OS kernels or relational databases use the Banker’s Algorithm? Because in interactive workloads (such as an SQL transaction executing arbitrary UPDATE statements based on user input), it is impossible to know a transaction’s maximum resource claim $\mathbf{Max}$ in advance, nor can row locks be acquired in a static global order. Consequently, RDBMS engines (PostgreSQL, MySQL InnoDB, Oracle, SQL Server) allow deadlocks to occur and rely on Deadlock Detection & Recovery or distributed Timestamp-Based Prevention (Wait-Die / Wound-Wait).

6.1 Wait-For Graphs (WFG) in PostgreSQL and MySQL InnoDB

When every resource (such as an exclusive row lock) is managed by a lock queue, the database collapses the bipartite Resource Allocation Graph into a Wait-For Graph (WFG) $G_{\text{WFG}} = (\mathcal{T}, E_{\text{wait}})$, where vertices are active transactions $\mathcal{T} = \{T_1, \dots, T_n\}$ and a directed edge $T_i \to T_j$ exists if and only if transaction $T_i$ is blocked waiting for transaction $T_j$ to release a conflicting lock.

In a WFG, a directed cycle is a necessary and sufficient condition for deadlock, detectable in $O(|V| + |E|)$ linear time using three-color Depth-First Search (DFS) or Tarjan’s Strongly Connected Components (SCC) algorithm:

  • Eager Detection (MySQL InnoDB): InnoDB triggers a WFG cycle search (or wakes a dedicated background deadlock-checker thread) immediately when a lock request blocks, aborting the transaction with the smallest Undo Log weight (fewest modified rows) as the Deadlock Victim (ERROR 1213 (40001): Deadlock found).
  • Lazy Timeout-Triggered Detection (PostgreSQL deadlock_timeout = 1s): Because $99.9\%$ of lock waits resolve within a few milliseconds, PostgreSQL avoids running expensive WFG graph traversals on every lock wait; instead, a blocked backend sleeps for deadlock_timeout ($1000\,\text{ms}$) before running the WFG detector (which even attempts Wait Queue Rearrangement before aborting a victim with SQLSTATE 40P01).

6.2 Distributed Deadlock Prevention: Wait-Die vs. Wound-Wait (Rosenkrantz et al., 1978)

In distributed databases (such as Google Spanner or CockroachDB), building a global Wait-For Graph across network nodes incurs high message latency and risks Phantom Deadlocks due to asynchronous network delays. To avoid global WFG construction, Daniel J. Rosenkrantz, Richard E. Stearns, and Philip M. Lewis (1978) invented two timestamp-ordered deadlock prevention schemes. Each transaction $T_i$ is assigned a unique start timestamp $\text{TS}(T_i)$:

$$T_i \text{ is OLDER than } T_j \iff \text{TS}(T_i) < \text{TS}(T_j)$$

Suppose transaction $T_i$ requests a lock currently held by transaction $T_j$:

  flowchart TD
    Req["Transaction Ti requests a lock currently held by Tj"] --> Cmp{"Compare Start Timestamps:<br/>Is TS(Ti) < TS(Tj)? (Is requester Ti OLDER than holder Tj?)"}

    Cmp -->|"Yes: Ti is OLDER"| WD1["[Wait-Die (Non-Preemptive)]<br/>Older Ti WAITS for younger Tj<br/>(Wait edge: Older -> Younger)"]
    Cmp -->|"No: Ti is YOUNGER"| WD2["[Wait-Die (Non-Preemptive)]<br/>Younger Ti DIES (aborts itself)<br/>& retries with ORIGINAL timestamp TS(Ti)"]

    Cmp -->|"Yes: Ti is OLDER"| WW1["[Wound-Wait (Preemptive)]<br/>Older Ti WOUNDS (preempts/aborts) younger Tj<br/>and seizes the lock immediately!"]
    Cmp -->|"No: Ti is YOUNGER"| WW2["[Wound-Wait (Preemptive)]<br/>Younger Ti WAITS for older Tj<br/>(Wait edge: Younger -> Older)"]
  • Why Neither Protocol Can Ever Deadlock:
    In Wait-Die, wait-for edges $T_i \to T_j$ only ever point from Older to Younger ($\text{TS}(T_i) < \text{TS}(T_j)$). In Wound-Wait, wait-for edges $T_i \to T_j$ only ever point from Younger to Older ($\text{TS}(T_i) > \text{TS}(T_j)$). Because timestamps are strictly monotonic along every path, a directed cycle is mathematically impossible!
  • Why Retaining the Original Timestamp Prevents Starvation, and Why Wound-Wait Beats Wait-Die:
    When a transaction aborts (via Die or Wound) and restarts, it must retain its original birth timestamp $\text{TS}(T_i)$. As time passes, a repeatedly aborted transaction eventually becomes the oldest transaction in the system (minimum $\text{TS}$) and can never be aborted again. Moreover, Wound-Wait causes far fewer wasted restarts than Wait-Die (which is why Google Spanner uses Wound-Wait): in Wait-Die, a younger transaction $T_{\text{young}}$ that dies and immediately restarts will likely collide with the still-running $T_{\text{old}}$ and die again in a tight loop; whereas in Wound-Wait, once $T_{\text{young}}$ is wounded by $T_{\text{old}}$ and restarts, it is now the younger requester asking for a lock held by $T_{\text{old}}$, so it calmly Waits instead of aborting again!

7. Strategy 4: The Ostrich Algorithm — Engineering Economics vs. Mathematical Purity

The fourth strategy, named The Ostrich Algorithm by Andrew S. Tanenbaum, is to “stick your head in the sand and pretend the problem does not exist.”

While this sounds like a joke to a pure mathematician, UNIX, Linux, Windows, and macOS deliberately apply the Ostrich Algorithm to user-space resource table exhaustion (such as process table slots or open file descriptors). If a workstation crashes once every 5 years due to a hardware power outage or kernel driver bug, paying a 10% CPU overhead on every single fork() and open() system call every day to prevent a theoretical deadlock that occurs once a decade is an irrational engineering trade-off.

StrategyCore MechanismCanonical AlgorithmPrimary AdvantagePrimary LimitationTypical Domain
1. PreventionStatically falsify $\ge 1$ Coffman conditionTotal Lock Ordering ($\phi(L_1) < \phi(L_2)$), lockdep, RCUZero runtime graph overhead; $100\%$ mathematical freedom from deadlockRequires static knowledge of lock hierarchyOS Kernels, C++/Rust/Go multi-threaded engines
2. AvoidanceDynamically reject requests leading to Unsafe StatesDijkstra’s Banker’s Algorithm ($O(m \cdot n^2)$)Higher resource utilization than rigid static ordering; no rollbacksRequires upfront knowledge of $\mathbf{Max}$ claims and fixed $n$Specialized embedded & batch resource controllers
3. Detection & RecoveryAllow deadlock, detect cycles, abort a victimWait-For Graph (WFG) DFS/Tarjan + Transaction RollbackAllows arbitrary dynamic lock acquisition order (SELECT ... FOR UPDATE)Requires transactional undo logs; victim work is wastedRDBMS (PostgreSQL, MySQL InnoDB, Oracle, SQL Server)
4. Ostrich AlgorithmIgnore ultra-rare deadlocks; rely on watchdog/rebootManual kill -9, process timeouts, watchdog timersZero daily runtime CPU or memory overheadRequires human or watchdog intervention if deadlock occursGeneral-purpose OS user-space resource limits

8. Deadlock vs. Livelock vs. Starvation: The Complete Liveness Taxonomy

In Leslie Lamport’s formal taxonomy of concurrent systems (1977), correctness consists of Safety Properties (“nothing bad ever happens,” e.g., no data races) and Liveness Properties (“something good eventually happens,” e.g., every request eventually completes). Deadlock has two sibling Liveness Failures that every systems engineer must distinguish:

8.1 Livelock: The Infinite Polite Corridor Dance and Randomized Exponential Backoff

Imagine two polite pedestrians meeting head-on in a narrow hallway. Both step to the north side at the exact same instant to let the other pass; seeing they are still blocked, both step to the south side at the exact same instant. Neither pedestrian is sleeping—both are actively moving and expending energy—yet neither makes any forward progress down the hallway.

In software, Livelock occurs when two threads attempt to avoid deadlock by using non-blocking trylock and releasing their held lock upon failure, without introducing randomness:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// Symmetric trylock retry loop without backoff -> LIVELOCK (100% CPU usage, 0 progress!)
void thread_1() {
    while (true) {
        mutex_lock(&lock_A);
        if (mutex_trylock(&lock_B) == SUCCESS) {
            critical_section();
            mutex_unlock(&lock_B);
            mutex_unlock(&lock_A);
            break;
        }
        mutex_unlock(&lock_A); // Politely release lock_A and retry immediately!
    }
}
  • Diagnostic Contrast (CPU & OS State): In a Deadlock, threads are in the kernel BLOCKED / SLEEPING state and consume $0\%$ CPU. In a Livelock, threads remain in the RUNNING / RUNNABLE state, spinning furiously and pegging CPU utilization at $100\%$ while achieving zero application throughput!
  • The Cure — Symmetry Breaking via Randomized Exponential Backoff with Jitter:
    Because livelock is driven by temporal lockstep symmetry, waiting a fixed delay (sleep(10ms)) fails because both threads wake up together $10\,\text{ms}$ later. The solution (used in Ethernet CSMA/CD and distributed lock managers) is to sleep for a random duration drawn uniformly from an exponentially growing window after $c$ collisions:
$$T_{\text{wait}} = \text{UniformRandom}\Bigl(0, \;\min\bigl(T_{\max}, \;T_{\text{base}} \cdot 2^c\bigr)\Bigr)$$

8.2 Starvation (Indefinite Postponement) and Aging

In Starvation, the system as a whole is making forward progress (not deadlocked or livelocked), but one or more specific threads are perpetually denied resources because higher-priority threads or continuous waves of reader threads (in a naive Reader-Writer lock) keep cutting ahead in line. Starvation is cured by FIFO Fair Queuing (such as MCS / CLH queue spinlocks or phase-fair RW locks) or Priority Aging (gradually increasing a waiting job’s priority as a function of its waiting time).

Diagnostic DimensionDeadlockLivelockStarvation
Core MechanismCircular wait among blocked threads holding non-preemptible resourcesSymmetric non-blocking retry loops continuously undoing each other’s stateUnfair scheduling or lock policy perpetually bypassing a waiting thread
OS Thread StateBLOCKED / SLEEPING (in kernel wait queue)RUNNING / RUNNABLE (actively executing instructions)BLOCKED or waiting in ready/lock queue
CPU Utilization of Victims$0\%$ (completely asleep)$\approx 100\%$ (busy-spinning and mutating state)$0\%$ for the starved thread; high for competing threads
Global System ProgressZero progress for all threads in deadlocked set $D$Zero forward progress for livelocked threads despite state transitionsOther threads make normal progress; only victim stalls
Primary Engineering CureTotal Lock Ordering ($\prec$), Banker’s Algorithm, WFG cycle abortRandomized Exponential Backoff with Jitter, Wound-Wait priorityPriority Aging, Fair FIFO queue locks (MCS / CLH / Ticket locks)
comments powered by Disqus