computers/deadlock-how-two-programs-can-wait-for-each-other-forever.md

Deadlock: how two programs can wait for each other forever

Computers · 5 min read ·

Two threads each hold one lock and wait for the other's. Neither can move, and the program just freezes without an error. This is a deadlock.

Step Ana and Ben through taking two locks in an order you choose, see exactly how they get stuck, and learn the one habit that prevents it: take locks in the same order everywhere. Includes real Python you can run.

Diagram: Ana and Ben each hold one lock and wait for the other's lock, crossing dashed arrows

Two friends, Ana and Ben, share one pen and one ruler. Ana grabs the pen and waits for the ruler. Ben grabs the ruler and waits for the pen. Nobody is doing anything wrong, yet nothing will ever happen again. Programs can get into exactly this situation, and it has a name: a deadlock.

Why programs wait for things

A program can do several jobs at once by splitting them into threads, which are separate lines of work running side by side. Sometimes two threads would damage shared data if they touched it at the same moment (an earlier article showed two threads adding 1 to 5 and ending up with 6). The usual fix is a lock: a thread must hold the lock before it touches the data, and anyone else who asks for the same lock has to wait until it is released.

One lock is easy to reason about. Trouble starts when a job needs two locks, for example "take money out of account A and put it into account B". The job takes lock A, then asks for lock B. If another job is doing the same thing in the opposite direction, it holds B and asks for A.

Try it: you decide who moves

Below, each worker has to take two locks, then finish and put both back. Nothing moves until you press a button, so you control the timing. First press Ana takes a step once and Ben takes a step once, with Ben's order set to "B then A". Then reset and try "A then B".

With opposite orders, there is a way to get stuck: Ana takes A, Ben takes B, and now each one needs the lock the other is holding. With the same order, you cannot get stuck, whatever sequence of buttons you press. Whoever takes A first simply makes the other wait, and then finishes and lets go.

The four ingredients

Computer scientists found that a deadlock needs all four of these at once (they are usually called the Coffman conditions, after the 1971 paper that listed them):

IngredientIn the example
Only one holder at a timeA lock can belong to one worker only
Hold and waitAna keeps A while asking for B
No taking it awayNobody can force Ben to give up B
A circle of waitingAna waits for Ben, and Ben waits for Ana

Break any one of the four and a deadlock cannot happen. That gives you real tools.

Seeing it in real code

This Python program starts two threads that take the same two locks in opposite orders. To keep the program from freezing forever, the second request gives up after one second:

import threading, time

lock_a = threading.Lock()
lock_b = threading.Lock()
say_lock = threading.Lock()

def say(text):
    with say_lock:
        print(text)

def worker(name, first, second):
    with first:
        say(f"{name} holds its first lock")
        time.sleep(0.2)
        if second.acquire(timeout=1):
            say(f"{name} got both locks")
            second.release()
        else:
            say(f"{name} waited 1 second for the second lock and gave up")

t1 = threading.Thread(target=worker, args=("Ana", lock_a, lock_b))
t2 = threading.Thread(target=worker, args=("Ben", lock_b, lock_a))
t1.start(); t2.start(); t1.join(); t2.join()

One real run on Python 3.13 printed:

Ana holds its first lock
Ben holds its first lock
Ana waited 1 second for the second lock and gave up
Ben got both locks

Both threads grabbed their first lock during the 0.2 second pause, so each one was now asking for the other's lock. Ana's timeout ran out slightly first; when she gave up, her with block released lock A, and Ben's pending request for A succeeded. The exact ending can differ from run to run, because it depends on timing. That is one of the nastiest things about deadlocks: they may show up only some of the time.

If you replace acquire(timeout=1) with a plain with second:, there is no timeout, and the two threads can wait on each other forever. The program just hangs with no error message.

How to avoid it

The most common fix is to break the circle: make every piece of code take locks in the same agreed order. Here, "always A before B":

def worker(name):
    with lock_a:
        say(f"{name} holds A")
        time.sleep(0.2)
        with lock_b:
            say(f"{name} got both locks")

Run with two threads, this printed, every time I tried it:

Ana holds A
Ana got both locks
Ben holds A
Ben got both locks

Other ways to break the four ingredients exist too. You can use a timeout and retry, as in the example above, which breaks the "no taking it away" ingredient in spirit. You can hold only one lock at a time, which breaks hold and wait. Or you can put the shared work behind one single owner thread that handles requests one by one, so nobody needs two locks.

What this means for your own code

If a program freezes completely, with no error and no CPU use, and it uses threads or locks, suspect a deadlock. Look for any place where code takes a second lock while it already holds one, and check whether another place takes the same two in the other order. Databases hit the same problem with row locks, and many of them detect the circle and cancel one of the waiting transactions so the other can go on. Having a rule such as "locks are always taken in alphabetical order" is dull, and that is exactly why it works.

$ ls computers/

see all →
computers/virtual-memory-why-two-programs-can-use-the-same-address-without-clash.md Virtual memory: why two programs can use the same address without clashing Two programs can both use "address 4096" without ever touching each other's data. The trick is that the addresses a program sees are… 6 min read · 29 Oct computers/fetch-decode-execute-the-three-beat-loop-every-cpu-repeats-billions-of.md Fetch, decode, execute: the three-beat loop every CPU repeats billions of times a second A processor looks clever, but at its core it repeats one small loop billions of times a second: fetch an instruction, decode what it asks… 5 min read · 26 Oct computers/the-call-stack-why-a-function-that-never-stops-crashes-your-program.md The call stack: why a function that never stops crashes your program Every time a function is called, the computer puts a small block of memory called a frame on top of a call stack, and removes it when the… 5 min read · 23 Oct computers/floating-point-why-0-1-0-2-is-not-exactly-0-3.md Floating point: why 0.1 + 0.2 is not exactly 0.3 Type `0.1 + 0.2` into JavaScript or Python and you get `0.30000000000000004`. It is not a bug, and it is not your mistake. Computers store… 5 min read · 19 Oct computers/race-conditions-why-two-people-adding-1-to-5-can-leave-you-with-6.md Race conditions: why two people adding 1 to 5 can leave you with 6 Two phones add one item to a shared list of 5, and the list ends up at 6 instead of 7. Nothing is broken, yet an update was lost. This is a… 6 min read · 16 Oct computers/bits-and-bytes-why-a-byte-can-t-count-past-255.md Bits and bytes: why a byte can't count past 255 A byte is eight on/off switches, and that tiny size explains a lot of odd bugs. In this article you flip the bits of one byte yourself and… 4 min read · 13 Oct