The Story That Explains Prevention & Avoidance
A customer walks in and asks for ₹3L on top of the ₹2L they already borrowed. The manager doesn't just look at "do I have ₹3L in the vault today?" — she asks the deeper question: "If I hand over this money right now, is there still a scenario in which every customer can eventually be fully repaid without any of them getting stuck waiting?"
If yes → grant. If no → politely ask them to wait, even though the money is physically available. This conservative rule is exactly Dijkstra's Banker's Algorithm. In an OS, "money" is CPU/memory/mutex/file-handles, and "customers" are processes.
Prevention makes deadlock structurally impossible — it removes one of the four Coffman conditions at design time, so no runtime check is ever needed. Avoidance lets all four conditions exist, but the OS makes each allocation decision carefully at runtime, refusing any request that could later lead to deadlock.
Quick Recap — The Four Coffman Conditions
Before we can break any condition, remember all four must simultaneously hold for a deadlock to occur. Prevention strategies attack these one by one.
| # | Condition | What It Means |
|---|---|---|
| 1 | Mutual Exclusion | At least one resource is non-sharable — only one process at a time. |
| 2 | Hold and Wait | A process holds some resources while requesting more. |
| 3 | No Preemption | Resources cannot be forcibly taken back — released voluntarily. |
| 4 | Circular Wait | A closed chain of processes exists, each waiting for the next. |
Pick any one of the four conditions and design your system so it cannot occur. Deadlock becomes mathematically impossible. Sections 3–6 examine each strategy in turn.
Prevention Strategy 1 — Break Mutual Exclusion
If a resource can be shared by multiple processes, no process ever needs to wait, and therefore no deadlock. Sadly, most interesting resources are intrinsically non-sharable: a printer cannot print two documents mid-page, a mutex cannot be locked twice.
| Read-only files — infinitely many readers, no writers → sharable |
| Spooled printers — jobs pile in a spool file, printer serves them serially |
| Immutable data structures — every "modification" produces a new copy |
| Mutex locks — the entire point is exclusive access |
| Write locks on files — cannot allow two concurrent writers |
| Physical single-user devices — CD burner, tape drive |
In general, we cannot prevent deadlocks by denying mutual exclusion because some resources are fundamentally exclusive. This strategy has limited applicability.
Prevention Strategy 2 — Break Hold and Wait
A process may not hold any resource while requesting others. Two protocols implement this.
Example Comparison
A copy program copies data from tape (T) to disk (D), then prints results (P). Under Protocol A, it grabs T + D + P at the start — even though P isn't used for hours of copying. Under Protocol B, it grabs T + D, does the copy, releases both, then grabs D + P and prints. The middle release point creates a race: another process might snatch D and the copy program blocks.
Prevention Strategy 3 — Break No Preemption
Allow the OS to forcibly take resources back from a waiting process. Two rules apply.
Preemption works for resources whose state can be saved and restored: CPU registers, memory pages. It does not work for printers (mid-page abort ruins the output) or write locks on files (partial writes may corrupt the file). Common in memory management, rare elsewhere.
Prevention Strategy 4 — Break Circular Wait (Most Practical)
Impose a total ordering on all resource types. Assign each type a unique integer F(Ri). Every process must request resources in strictly increasing order of F. To request a resource lower in the order, first release all resources of equal or higher order.
Suppose a circular wait exists: P0 → P1 → … → Pn → P0. This would mean Pi holds a resource with a higher F than the one it's waiting for. But every process only requests higher F values, never lower. Contradiction → no cycle can exist → no deadlock.
🎮 Interactive — Watch Ordered Locking Prevent Deadlock
Two processes both need R1 (order=1) and R2 (order=2). With the ordering rule enforced, both must acquire R1 first, then R2. Click Next to trace the execution.
Ordered locking is easy to implement, has zero runtime overhead, and requires no OS
cooperation — just a documented convention. Linux kernel code, PostgreSQL, and virtually
every large C++ codebase use this pattern. Static analysis tools like Linux's
lockdep automatically detect violations.
Deadlock Avoidance — Concept
Prevention is pessimistic — it disallows patterns that could deadlock even when they wouldn't. Avoidance is optimistic — it grants requests whenever safe and postpones only truly risky ones. The trade-off is that avoidance requires a priori information: each process must declare its maximum future resource need.
Safe State — Formal Definition
A state is safe if there exists a safe sequence ⟨P1, P2, …, Pn⟩ of all processes such that, for each Pi, the resources Pi may still request can be satisfied by:
Needi ≤ Available + Σj < i Allocationj
In plain English: Pi's remaining need can be met by currently free resources
plus everything held by processes that finish before it.
Banker's Algorithm — Overview
Dijkstra's Banker's Algorithm is the classic implementation of avoidance for systems with multiple instances of each resource type. Named for a bank that only lends when it can still guarantee all customers can be fully repaid.
The Banker's Algorithm only works if every process declares its maximum need in advance — and never exceeds it. This is often unrealistic in general-purpose operating systems, which is why avoidance is used mostly in real-time and embedded systems where workloads are known.
The Safety Algorithm
Given the current state (Available, Allocation, Need), the safety algorithm asks: "Does a safe sequence exist?" It simulates completion of processes in a hypothetical order.
# Determine whether the system is in a safe state
Work = Available # snapshot of free resources
Finish = [false] * n # has each process finished?
while exists i such that Finish[i] == false and Need[i] <= Work:
Work = Work + Allocation[i] # pretend Pi finishes, returns its resources
Finish[i] = true
if all(Finish):
return "SAFE — a safe sequence exists"
else:
return "UNSAFE — deadlock is possible"
Time complexity: O(m·n²) — for each of n selections we may scan up to n processes across m resource types. Runs every time a request arrives, so avoid for high-frequency allocation.
🎮 Interactive — Walk Through Banker's Safety Check (Galvin's Classic)
Setup: 5 processes (P0–P4), 3 resource types (A, B, C). Total instances: A=10, B=5, C=7. Currently allocated: A=7, B=2, C=5, so Available = [3, 3, 2]. Click Next to test one process at a time.
| A | B | C | |
|---|---|---|---|
| P0 | 0 | 1 | 0 |
| P1 | 2 | 0 | 0 |
| P2 | 3 | 0 | 2 |
| P3 | 2 | 1 | 1 |
| P4 | 0 | 0 | 2 |
| A | B | C | |
|---|---|---|---|
| P0 | 7 | 5 | 3 |
| P1 | 3 | 2 | 2 |
| P2 | 9 | 0 | 2 |
| P3 | 2 | 2 | 2 |
| P4 | 4 | 3 | 3 |
| A | B | C | |
|---|---|---|---|
| P0 | 7 | 4 | 3 |
| P1 | 1 | 2 | 2 |
| P2 | 6 | 0 | 0 |
| P3 | 0 | 1 | 1 |
| P4 | 4 | 3 | 1 |
The safe sequence ⟨P1, P3, P4, P0, P2⟩ exists. The system is in a SAFE state. Multiple safe sequences may exist — any one is sufficient to prove safety.
Resource-Request Algorithm
When process Pi makes a request Requesti, the Banker doesn't grant it blindly. It pretends to grant, runs the safety algorithm, and only truly commits if the result is safe. Otherwise, roll back and make Pi wait.
def resource_request(i, Request):
# Step 1 — sanity check: can't ask for more than declared max
if Request > Need[i]:
raise Error("Process exceeded its Max declaration")
# Step 2 — physical check: is there enough right now?
if Request > Available:
return "WAIT — not enough resources yet"
# Step 3 — pretend to grant
Available -= Request
Allocation[i] += Request
Need[i] -= Request
# Step 4 — safety check on the pretended state
if is_safe():
return "GRANTED" # commit the change
else:
# Roll back — request would lead to unsafe state
Available += Request
Allocation[i] -= Request
Need[i] += Request
return "DENIED — unsafe, must wait"
🎮 Interactive — Two Requests Handled by the Banker
Same initial state as before. Watch the Banker evaluate two requests: one is granted, the next is refused because it would drive the system into an unsafe state.
| Process | Allocation | Need | ||||
|---|---|---|---|---|---|---|
| A | B | C | A | B | C | |
| P0 | 0 | 1 | 0 | 7 | 4 | 3 |
| P1 | 2 | 0 | 0 | 1 | 2 | 2 |
| P2 | 3 | 0 | 2 | 6 | 0 | 0 |
| P3 | 2 | 1 | 1 | 0 | 1 | 1 |
| P4 | 0 | 0 | 2 | 4 | 3 | 1 |
Numerical Problem 1 — Fully Worked Static Example
Given the following system state, determine whether it is safe. If safe, find a safe sequence.
Problem Statement
| Number of processes: 4 (P0, P1, P2, P3) |
| Number of resource types: 3 (A, B, C) |
| Total instances: A = 8, B = 5, C = 6 |
| A | B | C | |
|---|---|---|---|
| P0 | 1 | 0 | 2 |
| P1 | 3 | 1 | 1 |
| P2 | 2 | 1 | 0 |
| P3 | 1 | 0 | 1 |
| A | B | C | |
|---|---|---|---|
| P0 | 4 | 3 | 3 |
| P1 | 4 | 2 | 3 |
| P2 | 5 | 2 | 2 |
| P3 | 5 | 3 | 3 |
| A | B | C | |
|---|---|---|---|
| P0 | 3 | 3 | 1 |
| P1 | 1 | 1 | 2 |
| P2 | 3 | 1 | 2 |
| P3 | 4 | 3 | 2 |
Step 1 — Compute Available
# Total allocated per column
Sum_A = 1 + 3 + 2 + 1 = 7
Sum_B = 0 + 1 + 1 + 0 = 2
Sum_C = 2 + 1 + 0 + 1 = 4
# Available = Total − Sum
Available_A = 8 − 7 = 1
Available_B = 5 − 2 = 3
Available_C = 6 − 4 = 2
Available = [1, 3, 2]
Step 2 — Run Safety Algorithm
Yes, the system is safe. A valid safe sequence is ⟨P1, P0, P2, P3⟩.
Numerical Problem 2 — Interactive Walkthrough
Consider a different system. Click Next to run the safety algorithm one step at a time.
Setup
| Processes: 4 (P0, P1, P2, P3) |
| Resource types: 3 (X, Y, Z) |
| Total: X = 9, Y = 3, Z = 6 |
| Available (already computed): [1, 1, 2] |
| Alloc | Max | Need | |||||||
|---|---|---|---|---|---|---|---|---|---|
| X | Y | Z | X | Y | Z | X | Y | Z | |
| P0 | 2 | 1 | 1 | 5 | 1 | 3 | 3 | 0 | 2 |
| P1 | 1 | 0 | 1 | 2 | 1 | 2 | 1 | 1 | 1 |
| P2 | 4 | 1 | 2 | 7 | 2 | 3 | 3 | 1 | 1 |
| P3 | 1 | 0 | 0 | 2 | 1 | 1 | 1 | 1 | 1 |
| X | Y | Z | |
|---|---|---|---|
| P0 | 2 | 1 | 1 |
| P1 | 1 | 0 | 1 |
| P2 | 4 | 1 | 2 |
| P3 | 1 | 0 | 0 |
| X | Y | Z | |
|---|---|---|---|
| P0 | 3 | 0 | 2 |
| P1 | 1 | 1 | 1 |
| P2 | 3 | 1 | 1 |
| P3 | 1 | 1 | 1 |
Advantages & Disadvantages
| Prevents deadlock without pessimistic restrictions on programming style |
| Allows the four Coffman conditions to all exist — natural code, natural mutexes |
| Provides a mathematical guarantee: if all requests stay within Max, deadlock is impossible |
| Works with multiple instances per resource type |
| Max declaration required — often unrealistic in general-purpose OS |
| Process count and resource count must be fixed — no dynamic joins |
| O(m·n²) overhead per request — expensive at high frequency |
| Processes must eventually return resources within finite time (must not hang mid-work) |
| Conservative — rejects some requests that would in practice be safe |
Comparison — Prevention vs Avoidance
| Aspect | Prevention | Avoidance (Banker's) |
|---|---|---|
| When to act | Design time / compile time | Runtime, per request |
| Attitude | Pessimistic | Optimistic — grants when safe |
| Runtime overhead | None | O(m·n²) per request |
| Requires max declaration? | No | Yes — up front |
| Device utilisation | Often low | Usually high |
| Best for | General-purpose OS, mutex-heavy code | Real-time, batch, embedded systems |
| Example | Ordered lock acquisition in Linux kernel | Real-time task schedulers with known workload |
Real-World Applications
lockdep runtime checker
dynamically tracks acquisition order and warns on any violation that could theoretically
cause deadlock.
ReentrantLock.tryLock(timeout) lets developers back out of a lock
attempt — effectively breaking the "no preemption" condition through voluntary release.