Skip to content

15/32 · 6 min

Two threads, two locks, no progress

A bank transfers money between accounts. It locks both, to be safe.

c
transfer(A, B):  lock(A); lock(B); move(); unlock(B); unlock(A);

Two transfers run at once. One moves money from A to B. The other moves money from B to A.

code
Thread 1: lock(A)  ok
Thread 2: lock(B)  ok
Thread 1: lock(B)  waits for thread 2
Thread 2: lock(A)  waits for thread 1

Neither will ever proceed. Nothing has crashed. The program simply stops, and in production it stops at 3am under load, having worked perfectly for months.

The four conditions

Deadlock needs all four at once. Break any one and it cannot happen:

  1. 1Mutual exclusion. A resource is held by one thread at a time.
  2. 2Hold and wait. A thread holds one resource while waiting for another.
  3. 3No preemption. A resource cannot be taken away, only given up.
  4. 4Circular wait. A cycle of threads each waiting on the next.

What you can actually do

Prevention removes a condition by design. The practical one is breaking circular wait: always take locks in the same order. Lock the lower account number first, and the example above cannot deadlock, because both threads reach for the same lock first.

Avoidance decides at runtime whether granting a request could lead to trouble. The Banker's algorithm does this, and it needs every process to declare its maximum needs in advance, which real programs cannot do.

Detection and recovery lets it happen, notices the cycle in the wait-for graph, and kills something. Databases do this: one transaction is chosen as the victim and rolled back.

Ignoring it is what most operating systems do. Linux does not detect deadlock between user threads. It is the programmer's problem, and the reasoning is that the cost of prevention exceeds the cost of the rare failure.

Two threads, two locks, no progress