Module 03 • COSC240

Deadlock Avoidance & Banker's Algorithm

Explore deadlock avoidance strategies through Edsger Dijkstra's Banker's Algorithm. Evaluate safety state invariants, resource request vectors, and matrix inequality tests.

1. Deadlock Prevention vs. Avoidance

When designing operating system resource management subsystems, concurrency architects face a fundamental choice between Deadlock Prevention and Deadlock Avoidance. Both strategies guarantee that the system will never enter a deadlocked state, but they achieve this security through radically different philosophical and architectural mechanisms.

1. Deadlock Prevention: Structural Constraint Enforcement

Deadlock prevention operates on a static principle: design the operating system rules such that at least one of the Four Coffman Conditions can never possibly be satisfied. By structurally prohibiting one of the necessary preconditions, deadlock becomes mathematically impossible.

2. Deadlock Avoidance: Dynamic State Inspection

Deadlock prevention often results in poor device utilization and unnecessary process blocking because its restrictions are overly conservative. Deadlock avoidance, by contrast, takes a more flexible approach. It permits processes to request resources dynamically, but subjects every request to a runtime safety audit.

Instead of constraining system rules upfront, the operating system requires each process to declare its maximum lifetime resource claims in advance (${\text{Max}}_{ij}$). Before granting any immediate resource request, the kernel evaluates the future trajectory of the system:

The Avoidance Invariant:

$$ \text{Grant Request} \iff \exists \text{ a safe execution sequence } \langle P_0, P_1, \dots, P_n \rangle \text{ such that all claims can be satisfied} $$

If granting a request leads to a safe state (where the kernel can guarantee every process eventually gets its maximum declared resources), the request is approved. If the allocation leads to an unsafe state (a state from which deadlock is mathematically possible if all processes suddenly demand their maximum claims), the kernel suspends the requesting process until resources can be safely allocated.

3. Architectural Trade-Offs

Metric Deadlock Prevention Deadlock Avoidance
A Priori Information Not required. Operates purely on runtime rules. Required. Processes must declare maximum resource claims upfront.
Resource Utilization Low to Moderate (due to strict static constraints). High (resources allocated dynamically based on live needs).
Runtime Overhead Minimal (simple rule enforcement). Moderate to High ($O(m \times n^2)$ matrix safety checks per request).

2. Dijkstra's Banker's Algorithm

Formulated by Edsger Dijkstra in 1965 for the THE multiprogramming system, the Banker's Algorithm is the classical resource allocation and deadlock avoidance method. The algorithm is metaphorically named after a small-town banker who must allocate cash liquidity among clients while ensuring the bank never exhausts its reserves in a way that leaves clients unable to complete their financial projects.

1. Core Mathematical Data Structures

Let n be the total number of concurrent execution threads in the system, and let m be the total number of distinct resource types. The operating system kernel maintains four primary data structures in protected memory:

2. The Safety Algorithm

The kernel executes the Safety Algorithm to determine whether the system is currently in a safe state. A state is safe if there exists a safe execution sequence $\langle P_0, P_1, \dots, P_{n-1} \rangle$ such that every thread's maximum resource demands can be satisfied using currently available resources plus the resources released by preceding threads.

  1. Let $Work$ be a vector of length m initialized to $Available$, and let $Finish$ be a boolean vector of length n initialized to false for all threads.
  2. Find an index i such that: $$ Finish[i] == \text{false} \quad \land \quad Need_i \le Work $$ If no such index i exists, proceed to Step 4.
  3. If an eligible thread i is found, simulate its successful completion by returning its held resources to the working pool: $$ Work = Work + Allocation_i $$ $$ Finish[i] = \text{true} $$ Return to Step 2 to inspect remaining unfinished threads.
  4. If $Finish[i] == \text{true}$ for all threads i, the system is in a safe state, and the sequence of discovered indices forms a valid safe execution path. Otherwise, the system is unsafe.

3. The Resource-Request Algorithm

When thread $P_i$ issues a dynamic request vector $Request_i$ for additional resources, the kernel evaluates the request before modifying allocation tables:

  1. Claim Validation: Verify that $Request_i \le Need_i$. If a thread requests more than its declared maximum claim, raise a hardware fault.
  2. Availability Check: Verify that $Request_i \le Available$. If sufficient instances are free, the thread must wait.
  3. State Simulation: Temporarily assume the kernel grants the request by updating system state vectors: $$ Available = Available - Request_i $$ $$ Allocation_i = Allocation_i + Request_i $$ $$ Need_i = Need_i - Request_i $$
  4. Safety Audit: Run the Safety Algorithm on the hypothetical state. If the resulting state is safe, the resource allocation is finalized and granted to $P_i$. If unsafe, the state is rolled back, and $P_i$ is suspended until resources can be allocated safely.

4. Algorithmic Complexity & Python Implementation

The Banker's Algorithm safety check requires inspecting an $n \times m$ matrix across multiple passes, yielding a time complexity of $\mathcal{O}(m \times n^2)$. Below is a complete, syntax-highlighted Python implementation of the safety verifier:

python • bankers_verifier.py
#!/usr/bin/env python3
def evaluate_safety(available, allocation, max_matrix):
    num_threads = len(allocation)
    num_resources = len(available)

    need = [
        [max_matrix[i][j] - allocation[i][j] for j in range(num_resources)]
        for i in range(num_threads)
    ]

    work = list(available)
    finish = [False] * num_threads
    safe_sequence = []

    while len(safe_sequence) < num_threads:
        found_eligible = False
        for i in range(num_threads):
            if not finish[i] and all(need[i][j] <= work[j] for j in range(num_resources)):
                work = [work[j] + allocation[i][j] for j in range(num_resources)]
                finish[i] = True
                safe_sequence.append(i)
                found_eligible = True
                break
        if not found_eligible:
            return False, []

    return True, safe_sequence

if __name__ == "__main__":
    avail = [3, 3, 2]
    alloc = [[0,1,0], [2,0,0], [3,0,2], [2,1,1], [0,0,2]]
    max_mat = [[7,5,3], [3,2,2], [9,0,2], [2,2,2], [4,3,3]]

    is_safe, seq = evaluate_safety(avail, alloc, max_mat)
    print("System Safe:", is_safe, "Safe Sequence:", seq)
Interactive Walkthrough: Banker's Algorithm Safety State Stepper
Trace step-by-step how the Banker's Algorithm evaluates process needs against available resource vectors.
Step 1: Initialization. Initial system vectors loaded. Available = [3, 3, 2]. Evaluating initial Need matrix vectors against Available.
Phase: 1/4
Available: [3, 3, 2]
Checked: None
State: Safe Initial
MODEL:
Synchronized Visual Canvas — System Safety Vectors
ProcAllocationMaxNeed
P0[0, 1, 0][7, 5, 3][7, 4, 3]
P1[2, 0, 0][3, 2, 2][1, 2, 2] ✓
P2[3, 0, 2][9, 0, 2][6, 0, 0]
P3[2, 1, 1][2, 2, 2][0, 1, 1]
P4[0, 0, 2][4, 3, 3][4, 3, 1]
Active Safe Sequence Progress: [ P1 ] → Pending...
🔍 What Is Happening
Process P1 has Need [1, 2, 2] ≤ Available [3, 3, 2]. P1 can execute, finish, and release its allocation [2, 0, 0] back to Available.
⚙ Why The System Does This
Simulating completion guarantees that granting current resource claims preserves at least one valid execution sequence, avoiding deadlocked states.