Deadlock Lab
Four conditions have to hold simultaneously for a deadlock to exist. Break any one and it cannot happen. Three of the four are impractical to break in real code, which is why lock ordering is the answer you will actually reach for — and why it is the answer that has to be written down.
Build a deadlock yourself
Acquire and release locks by hand and watch the wait-for graph fill in. The moment a cycle closes, nothing in the system can proceed — and no participant is doing anything wrong.
A global lock order is a proof, not a habit
The practical fix, applied. Impose a total order on the locks and the cycle becomes unconstructible.
Prove it on a graph
The smallest deadlock there is, and the smallest fix.
Task 1 always acquires A then B. Flip the switch to change what Task 2 does. The cycle detector below is the same function the deadlock simulator uses — it is not told the answer, it walks the graph.
The four conditions, and which technique breaks each
All four must hold at once. Every deadlock-prevention technique is an attack on exactly one of them — and each attack has a price, which is why only one of them is common.
| Condition | What it means | What breaks it | What that costs |
|---|---|---|---|
| Mutual exclusion | The resource cannot be shared — one holder at a time, by definition. | Remove the sharing. Immutable data, a per-task copy, or a lock-free structure means there is nothing to hold exclusively. | Copies cost memory and can cost consistency; lock-free code costs an enormous amount of care and is rarely worth it for this reason alone. |
| Hold and wait | A task holds one resource while blocking for another. | Acquire everything at once or nothing at all — a single combined lock, or a try-acquire-all that backs off and releases on failure. | Coarser locking reduces concurrency; the all-or-nothing retry can livelock if every task backs off in lockstep. |
| No preemption | A resource cannot be taken away from whoever holds it. | Timed acquisition. try_lock with a deadline: give up, release what you hold, and start over. | The work done before the timeout is thrown away, and a timeout that is too short turns a slow path into a livelock. |
| Circular wait | A cycle in the wait-for graph: each participant waits on a resource the next one holds. | A global lock order. Every task acquires locks in the same total order, so no cycle can form. This is the one that is practical in real code. | Someone has to define, document and enforce the order, and every new lock has to be placed in it — including locks inside libraries you did not write. |
What a deadlock is not
Three failures that look identical from outside and need completely different fixes.
- Every task is running. CPU is busy. Nothing advances.
- Typically a back-off-and-retry loop where everyone backs off in lockstep and collides again.
- A cycle detector finds nothing, because nobody is blocked.
- The system makes progress. One participant never does.
- Usually an unfair lock, a priority scheme, or a writer behind an endless stream of readers.
- Throughput looks healthy; the p99.9 of one caller does not.
- One task holds a lock across an I/O call and everyone queues behind it.
- No cycle, no unfairness — a critical section that is simply far too wide.
- Lock wait time rises with load; the fix is to shrink the section, not to add threads.