Deadlock Prevention, Avoidance, and Detection

Imagine two database transactions, each holding a lock the other needs. Transaction A locks row 1 and waits for row 2. Transaction B locks row 2 and waits for row 1. Neither can proceed. Neither will ever release what it holds. This is Deadlock in its purest form. It is not a rare bug. It is a structural possibility any time multiple threads acquire multiple exclusive resources in an uncoordinated order. Operating systems and database engines cannot simply hope this never happens. A system manages disk drives, printers, locks, semaphores, and memory buffers. It does this for dozens of concurrent processes. Such a system must have an explicit policy for deadlock. The alternative is a system that occasionally freezes forever. There is no diagnostic and no recovery path. The stakes are concrete. A stalled database transaction can halt an entire e-commerce checkout pipeline. A deadlocked kernel thread can hang an entire machine. This article is about three qualitatively different engineering philosophies for handling deadlock. First, understand why it happens: the four Coffman Conditions. These are mutual exclusion, hold-and-wait, no preemption, and circular wait. Prevention attacks the structural conditions. This makes deadlock impossible by construction. Avoidance allows the conditions to exist. It uses runtime foresight to steer the system away from dangerous states. Detection and recovery lets deadlock happen, then finds it and breaks it. This accepts the cost of occasional rollback in exchange for cheaper day-to-day operation. By the end, you will be able to design a locking protocol that provably cannot deadlock. You will trace through the resource-allocation graph that underlies detection algorithms. You will understand exactly what information avoidance schemes need that prevention and detection do not. This is the groundwork for the Banker's Algorithm, the canonical avoidance algorithm covered next.

Key takeaways
  • Deadlock requires all four Coffman conditions simultaneously; breaking any one prevents deadlock entirely.
  • Prevention restricts how processes request resources (e.g., ordering, all-at-once acquisition) so a cycle can never form.
  • Avoidance permits requests but rejects any that would lead to an unsafe state, using look-ahead over maximum claims.
  • Detection lets deadlocks occur, then finds them via cycle-detection over a resource-allocation or wait-for graph.
  • Recovery kills or rolls back processes, or preempts resources, to break a detected cycle.
  • Each strategy trades runtime overhead and information requirements against concurrency and throughput.
The core insight

Deadlock cannot exist unless mutual exclusion, hold-and-wait, no-preemption, and circular-wait all hold at once — so every deadlock strategy works by making one of them structurally false, provably avoided, or promptly detected and broken.

The idea

A deadlock is a state in which a set of processes are each waiting for a resource held by another process in the same set, so that no process can ever proceed. Formally, deadlock is possible only when four conditions hold simultaneously: mutual exclusion (resources are non-shareable), hold-and-wait (a process holds resources while requesting more), no preemption (a resource cannot be forcibly taken from its holder), and circular wait (a cycle of processes each waiting on the next). These are necessary conditions — deadlock cannot occur without all four — but together, in the presence of a cycle, they are also sufficient. This is the exact fact every strategy below exploits. The three families of strategy differ in when they act and how much information they require. Prevention acts at design time: the system is engineered so that at least one Coffman condition can never arise, regardless of process behavior. Avoidance acts at request time: the system knows, in advance, the maximum resources each process might ever need, and uses that knowledge to simulate the future before granting any request, refusing requests that would lead to an unsafe state. Detection and recovery acts after the fact: requests are granted greedily with no look-ahead, and periodically (or on suspicion of stall) the system runs a graph algorithm to check whether a deadlock has actually formed, then intervenes. A fourth stance — ignore it (the "ostrich algorithm") — is common in general-purpose operating systems like a plain Unix kernel, where deadlocks are rare enough in practice that the cost of prevention/avoidance/detection outweighs the cost of an occasional reboot. This is a legitimate engineering tradeoff, not a failure to understand the problem; it is why deadlock handling is emphasized far more in database systems and real-time/embedded kernels, where correctness guarantees matter more than average-case throughput.

How it works

Prevention: negate one Coffman condition

Each of the four conditions can be attacked directly.

Attack mutual exclusion. Make resources shareable where possible (e.g., read-only files, spooling for printers so processes never wait on the physical device itself). This is limited — some resources (a write lock, a CPU core) are inherently exclusive — but wherever it applies it eliminates deadlock on that resource entirely.

Attack hold-and-wait. Require a process to request all resources it will ever need before execution begins, or require it to release everything it holds before requesting more. Either rule structurally prevents a process from holding some resources while blocking on others.

Attack no preemption. If a process holding resources requests another that is unavailable, forcibly preempt (take away) its currently held resources, or preempt from other waiting processes. The state is later restored, or the process is rolled back. This works well for resources whose state is easy to save and restore (CPU registers, memory pages) and poorly for resources that are not (a partially written file, a printer mid-job).

Attack circular wait. Impose a total (or partial) ordering on all resource types, and require every process to request resources only in strictly increasing order of this ranking. This is by far the most widely used prevention technique in real systems, because it is cheap and composable. ```pseudocode // Resource ordering (attacks circular wait) // F: ResourceType -> Integer (a fixed total order, chosen once for the whole system) function request(process P, resource R): if R.rank <= P.highest_held_rank: // Would create a request pointing "backward" in the order error("must request in increasing rank order") acquire(R) P.highest_held_rank = R.rank // Correctness sketch: // Suppose a cycle P1 -> P2 -> ... -> Pk -> P1 existed, where Pi -> Pi+1 // means "Pi holds a resource that Pi+1 is waiting for." // Each edge Pi -> Pi+1 implies rank(resource held by Pi) < rank(resource requested by Pi+1) // which Pi+1 must hold before requesting further resources of higher rank, ultimately // requiring rank(held by Pi+1) < rank(requested from Pi+2) ... and so on around the cycle. // Following the cycle all the way around forces // rank_1 < rank_2 < ... < rank_k < rank_1 // a strict cycle of real numbers, which is impossible. Hence no cycle, hence no deadlock. ``` The proof above is the essential reasoning: a total order on resource acquisition forbids a cycle in the wait-for relation because cycles would require ranks to increase strictly around a loop, which is impossible for real numbers. This is why resource ordering is the workhorse technique in lock-heavy code (e.g., always lock the object with the smaller memory address first).

Avoidance: simulate before granting

Avoidance is more permissive than prevention: it does not forbid hold-and-wait or circular structure outright. Instead, each process declares in advance its maximum claim for each resource type, and the system dynamically checks, before granting any request, whether granting it could ever lead to a state from which no sequence of future allocations can satisfy every process. Such a bad state is called unsafe; a state from which some ordering of process completions is guaranteed to exist is called safe. ```pseudocode // Safety check core idea (generalized; specialized fully in the Banker's Algorithm) function isSafe(allocation, available, maxClaim): work = copy(available) finish = [false] n while true: progressed = false for each process Pi with finish[Pi] == false: need_i = maxClaim[Pi] - allocation[Pi] if need_i <= work: // component-wise work = work + allocation[Pi] // simulate Pi finishing, releasing all it holds finish[Pi] = true progressed = true if all(finish): return true // safe: this completion order exists if not progressed: return false // unsafe: no process could finish with current 'work' ``` Before actually granting any request, the system tentatively grants it, runs `isSafe`, and rolls back the tentative grant if the result is unsafe — the request is instead deferred until it would* be safe. This is exactly the mechanism used in the Banker's Algorithm, which specializes this general safety check to multiple resource types with integer counts, and is the natural next topic after mastering this idea.

Detection and recovery: catch it after it happens

Detection abandons look-ahead. Requests are granted whenever resources are free, with no safety check. Periodically (or when a process seems to be waiting too long) the system builds a graph and searches for a cycle. For single-instance resource types (each resource type has exactly one unit — one lock, one file handle), build a wait-for graph: an edge `Pi -> Pj` means `Pi` is waiting for a resource currently held by `Pj`. Deadlock exists exactly when this graph has a cycle. ```pseudocode // Deadlock detection, single-instance resources function detectDeadlock(waitForGraph): visited = {}, inStack = {} for each node P in waitForGraph: if P not in visited: if dfs(P, visited, inStack, waitForGraph): return true // cycle found => deadlock return false function dfs(P, visited, inStack, graph): visited[P] = true inStack[P] = true for each neighbor Q of P in graph: if Q in inStack: return true // back edge => cycle if Q not in visited: if dfs(Q, visited, inStack, graph): return true inStack[P] = false return false ``` For multiple-instance resource types, the wait-for graph is insufficient (a cycle can exist without deadlock, if enough spare instances exist to eventually satisfy everyone). Instead run an algorithm structurally identical to the safety check above, but using each process's actual current request instead of its maximum claim: ```pseudocode // Deadlock detection, multiple-instance resources function detect(allocation, request, available): work = copy(available) finish = [false] * n for each process Pi: finish[Pi] = (allocation[Pi] == 0) // processes holding nothing can't be stuck while true: progressed = false for each process Pi with finish[Pi] == false: if request[Pi] <= work: work = work + allocation[Pi] finish[Pi] = true progressed = true if not progressed: break deadlocked = [Pi for Pi where finish[Pi] == false] return deadlocked // empty list means no deadlock ``` Once deadlocked processes are identified, recovery proceeds by process termination (kill all deadlocked processes at once, or kill them one at a time, re-running detection after each kill until the cycle is broken) or by resource preemption (forcibly take a resource from one process, roll it back to a safe checkpoint, and give the resource to another), being careful to avoid starvation by not always picking the same victim.

Figure
Wait-for graph before and after breaking a cycle
Deadlocked (cycle exists) P1 P2 P3 P1 waits on P2, P2 waits on P3, P3 waits on P1 After preempting P3's resource P1 P2 P3 P3 rolled back; cycle broken, P1 & P2 can finish

Complexity & correctness

Prevention (resource ordering). Each `request` call does O(1) work (a comparison and an update). No global search is ever needed at runtime, because the correctness argument is a static invariant proven once, at design time, over the fixed total order. This is the cheapest of all three strategies at runtime, but it is the least flexible: it requires all resource types to be known and ranked in advance, and it can force processes to acquire resources earlier than needed, hurting concurrency (a form of artificial hold-and-wait even while structurally preventing cyclic hold-and-wait).

Avoidance (safety checking). With nn processes and mm resource types, one `isSafe` call is O(n2m)O(n^2 m) in the worst case: the outer `while` loop runs at most nn times (each iteration finishes at least one process, or the loop terminates), and each iteration scans all nn processes comparing vectors of length mm. Since a safety check is run on every resource request, the amortized cost dominates system throughput far more than prevention's O(1) check. Correctness follows directly from the definition of a safe state: `isSafe` returns true exactly when a completion ordering exists, because the simulation greedily finds some valid order if one exists — if some process's need is satisfiable, granting it and adding its resources back to `work` only increases what is available for the rest, so greedily satisfying any eligible process is always a safe choice (an exchange argument: any valid completion order can be reordered to defer no eligible process without breaking validity).

Detection (cycle finding). Building and searching the wait-for graph for a cycle is O(V+E)O(V + E) using depth-first search, where VV is the number of processes and EE the number of wait-for edges — standard graph cycle detection. The multiple-instance detection algorithm has the same O(n2m)O(n^2 m) shape as the safety check, since it is structurally the same greedy simulation, this time against actual requests rather than maximum claims. Correctness: the algorithm finds a set of processes that can never be satisfied because at every step it only marks a process "finished" when its exact current request is satisfiable from `work`; any process left unmarked when no further progress is possible is, by definition, permanently blocked given the current allocation, because `work` only grows (never shrinks) across iterations, so if it could not satisfy a process now, it never will without external intervention.

Honest tradeoffs. Prevention costs concurrency (over-conservative acquisition) for free runtime checks. Avoidance costs runtime CPU (repeated O(n2m)O(n^2 m) checks) and requires each process to declare a maximum claim in advance — information that is often unavailable or overestimated, again hurting concurrency. Detection costs nothing on the common path but requires a recovery mechanism (rollback, checkpoint, or kill) that can be expensive or lossy, and the frequency of running detection is itself a tuning knob: too rare and processes stall a long time undetected, too frequent and detection overhead dominates.

Worked example

Consider a system with three resource types, A (10 instances), B (5 instances), C (7 instances), and three processes with the following current allocation and outstanding request (not maximum claim — this is a detection scenario, not avoidance):

ProcessAllocation (A,B,C)Request (A,B,C)
P0(0,1,0)(0,0,0)
P1(2,0,0)(2,0,2)
P2(3,0,3)(0,0,0)

Total allocated: A = 0+2+3 = 5, B = 1+0+0 = 1, C = 0+0+3 = 3. Available = Total − Allocated = (10−5, 5−1, 7−3) = (5, 4, 4). Run `detect`:

  • Initialize `work = (5,4,4)`. `finish[P0] = false` (holds B), but its request is (0,0,0) ≤ work trivially — actually mark eligibility by request, not allocation: P0's allocation is nonzero, but that does not block finishing.
  • Check P0: request (0,0,0) ≤ (5,4,4)? Yes. Finish P0, `work += allocation[P0] = (0,1,0)` → `work = (5,5,4)`.
  • Check P1: request (2,0,2) ≤ (5,5,4)? Yes. Finish P1, `work += (2,0,0)` → `work = (7,5,4)`.
  • Check P2: request (0,0,0) ≤ (7,5,4)? Yes. Finish P2, `work += (3,0,3)` → `work = (10,5,7)`.
  • All finished. `deadlocked = []` — no deadlock, despite P1 holding resources and requesting more.

Now suppose P2's request changes to (0, 0, 1) — it now also wants one more unit of C — while everything else is unchanged, and additionally P0's allocation is revised to (0, 1, 0) with request (0,0,0) as before, but Available is recomputed with one less spare C: Available = (5, 4, 3).

  • Check P0: request (0,0,0) ≤ (5,4,3)? Yes. Finish. `work = (5,5,3)`.
  • Check P1: request (2,0,2) ≤ (5,5,3)? No (needs 2 units of C, only 3 available but P1 also needs the 2 A's fine — wait: 2 ≤ 3 in C is true). Recheck carefully: (2,0,2) ≤ (5,5,3) is true component-wise. Finish P1. `work += (2,0,0)` → `work = (7,5,3)`.
  • Check P2: request (0,0,1) ≤ (7,5,3)? Yes. Finish. `work += (3,0,3)` → `work=(10,5,6)`.
  • Still no deadlock.

To actually force deadlock in this small system, shrink availability further: suppose instead Available starts at (0, 0, 0) because all instances are allocated, and P1's request is (2,0,2) while no other process can proceed. Then `work=(0,0,0)`; P0's request (0,0,0) ≤ (0,0,0) — P0 finishes, releasing (0,1,0), `work=(0,1,0)`; P1's request (2,0,2) ≤ (0,1,0)? No. P2's request (0,0,0) ≤ (0,1,0)? Yes, P2 finishes releasing (3,0,3), `work=(3,1,3)`. Now recheck P1: (2,0,2) ≤ (3,1,3)? Yes — P1 finishes too. Still no deadlock: this illustrates that with only three processes and modest requests, a genuine deadlock needs a request that no release can ever satisfy, e.g., if P1 requested (2,0,2) but total C instances were only 3 and all 3 were permanently held by P1 and P2 together with none releasable because P2's request itself depends on a resource only P1 holds — a true circular dependency, confirming why the algorithm's "no progress possible" branch, not merely a numeric shortfall, is the actual deadlock signature.

Common pitfalls & traps

PitfallWhy it happensFix
Confusing "cycle in wait-for graph" with "deadlock" for multi-instance resourcesA cycle can exist but still be resolvable if enough spare instances existUse the matrix-based detection algorithm for multi-instance resources, not raw cycle search
Partial resource ordering that misses a resource typeDeadlock still possible on the unranked resourceEvery resource type acquired anywhere in the system must be included in the global order
Running avoidance without true maximum claimsProcesses under-declare to seem "safer," then request more than declaredEnforce hard limits; reject any request exceeding the declared maximum outright
Detection run too infrequentlyDeadlocked processes sit stalled a long time, wasting held resourcesTune detection frequency to workload; trigger on suspected stalls (e.g., timeout) as well as periodically
Always picking the same recovery victimLeads to starvation of one unlucky processFactor in process age/priority/rollback cost when selecting a victim, and bound repeated selection
Treating "safe state" as "deadlock-free forever"Safe only guarantees some completion order exists for current claimsRe-run the safety check on every new request, not just once

Pioneers & history

The theory of deadlock was formalized as operating systems began supporting true multiprogramming, when multiple jobs competing for tape drives, memory, and card readers made cyclic waits a real operational hazard rather than a theoretical curiosity. The formal statement of the four necessary conditions is credited to Edward G. Coffman Jr., whose 1971 survey with Elphick and Shoshani crystallized decades of scattered operating-system folklore into a rigorous framework still taught today.

Origins · 1 minEdward G. Coffman Jr. and how this was discovered
EG
Edward G. Coffman Jr.
1934–2019 · Columbia University and Bell Labs, computer scientist known for foundational work in operating systems scheduling and resource allocation theory

Coffman's 1971 survey "System Deadlocks," co-authored with M. Elphick and A. Shoshani, gave the field its standard vocabulary: the four necessary conditions, the resource-allocation graph, and a taxonomy of prevention, avoidance, and detection strategies that remains structurally unchanged in every operating systems course since.

History
1965
— Edsger Dijkstra formulates the Banker's Algorithm concept while working on resource scheduling for the THE multiprogramming system.
1971
— Coffman, Elphick, and Shoshani publish "System Deadlocks," formalizing the four necessary conditions and the general strategy taxonomy.
1972
— Habermann and others extend safety-state analysis to more general multi-resource allocation models.
1980
s — Database systems popularize wait-for graph cycle detection and timeout-based deadlock detection for concurrent transactions.
2000
s–present — Distributed deadlock detection (edge-chasing, path-pushing algorithms) becomes essential for distributed databases and microservice lock managers.

Where this matters

Relational database engines run continuous or periodic deadlock detection on their internal lock managers, aborting the "cheapest" transaction (by rollback cost or age) when a cycle is found — this is why an application can receive a serialization/deadlock error and must retry the transaction. Kernel-level lock ordering, using techniques identical to the resource-ordering prevention scheme above, is enforced in production operating system kernels (Linux's lockdep subsystem dynamically verifies that no code path violates a global lock acquisition order, catching potential deadlocks even when they have not yet occurred). Distributed systems extend detection across machines with edge-chasing or path-pushing protocols, since no single node can see the whole wait-for graph. Real-time and embedded systems, where an unresolvable freeze can be safety-critical, more often lean on prevention (e.g., the priority ceiling protocol, an extension of resource ordering combined with priority inheritance) because it offers deadlock-freedom as a static, provable guarantee rather than a probabilistic runtime check. Understanding these three strategies — and the safety-state reasoning that underlies avoidance — is exactly the prerequisite for the Banker's Algorithm, the most detailed and historically important instance of the avoidance approach.

Ask the tutor: How exactly does the Banker's Algorithm's request-handling protocol use this safety check to decide whether to grant or defer a single incoming resource request?

Can a system use more than one deadlock strategy at once?

Yes, and real systems often do. A database might use lock ordering (prevention) for its own internal metadata locks, where the resource set is small and known in advance, while running periodic detection over user transaction locks, where request patterns are unpredictable and enforcing strict ordering would be impractical. A kernel might apply strict prevention to a fixed set of critical subsystem locks (verified statically by tools like lockdep) while leaving user-space synchronization primitives to detection or the ostrich approach. The right mix depends on how predictable resource requests are and how costly recovery would be if detection is chosen instead.

Why don't general-purpose operating systems just always use the Banker's Algorithm?

The Banker's Algorithm requires every process to declare, in advance, the maximum amount of every resource type it will ever need. In a general-purpose OS running arbitrary user programs, this information is usually unavailable, unreliable, or so overestimated that it defeats the purpose — a process claiming "I might need all the memory" makes every subsequent request look potentially unsafe. Avoidance is far more practical in closed, well-specified environments (a fixed set of jobs in an embedded controller, or a database's own internal resource pools) where maximum claims are genuinely knowable and stable.

Is it ever correct to just ignore deadlock entirely?

Yes — this is the so-called ostrich algorithm, and it is a legitimate engineering choice, not negligence, when deadlocks are empirically rare, recovery is cheap (reboot), and the overhead of prevention/avoidance/detection would cost more in everyday performance than the rare deadlock costs in downtime. Most general-purpose operating system kernels take exactly this stance for most user-level resource contention, reserving stricter guarantees only for subsystems where a freeze is unacceptable, such as internal kernel locking paths.

How does preemption-based recovery decide what to roll back to?

The system needs a checkpoint — a previously saved, consistent state of the victim process or transaction — to roll back to safely; rolling back to an arbitrary earlier point risks losing correctness if other processes have already observed effects that depended on work now being undone. In practice this means databases use write-ahead logs and savepoints, while OS-level resource preemption is typically limited to resources with cheap, well-defined save/restore semantics, like CPU registers or memory pages, rather than resources with side effects, like a partially printed document.

What is starvation in the context of deadlock recovery, and how is it avoided?

If a recovery policy always chooses the same victim process (say, the one holding the most resources, every time), that process can be repeatedly rolled back indefinitely while other processes keep completing around it — this is starvation, a liveness failure distinct from deadlock itself. The standard fix is to include an aging factor in victim selection, ensuring a process's priority for being spared increases the more times it has already been rolled back, guaranteeing every process eventually gets to run to completion.

Does deadlock prevention ever hurt performance even though it's "free" at request time?

Yes. While each individual `request` check is O(1), prevention techniques like requesting all resources up front (attacking hold-and-wait) force processes to acquire resources earlier and hold them longer than they strictly need, reducing overall system concurrency. Similarly, strict resource ordering can force a process to acquire a low-ranked resource well before it is used, just to satisfy the ordering constraint. The runtime check is cheap, but the design-level cost is reduced parallelism — a real tradeoff, not a free lunch.

How does this connect to what comes next, the Banker's Algorithm?

The general safety-check pseudocode shown here — simulate every process finishing in some order using declared maximum claims, and check whether all can eventually finish — is the conceptual core of the Banker's Algorithm. The Banker's Algorithm simply specializes this general idea to a concrete, fully worked-out data model (allocation matrix, max matrix, need matrix, available vector) and specifies the exact request-handling protocol around it, including how to safely test a single incoming request before committing to it.

Before this

Where this leads