What is a deadlock?
A deadlock happens when threads wait forever because each holds a resource the other needs.
Example deadlock
import threading
import time
lock_a = threading.Lock()
lock_b = threading.Lock()
def t1():
with lock_a:
time.sleep(0.1)
with lock_b:
print("t1")
def t2():
with lock_b:
time.sleep(0.1)
with lock_a:
print("t2")
threading.Thread(target=t1).start()
threading.Thread(target=t2).start()This can deadlock because each thread holds one lock and waits for the other.
How to prevent deadlocks
- Lock ordering: always acquire locks in the same order.
- Timeouts: use
lock.acquire(timeout=...). - Keep critical sections short.
- Prefer higher-level concurrency tools (Queue, Executor) when possible.
Example: lock ordering fix
import threading
lock_a = threading.Lock()
lock_b = threading.Lock()
def safe():
# Always acquire A then B
with lock_a:
with lock_b:
print("safe")
threads = [threading.Thread(target=safe) for _ in range(5)]
for t in threads:
t.start()
for t in threads:
t.join()The four ingredients
A deadlock needs all of these at once: a resource only one thread can hold at a time (a lock), a thread that holds one resource while waiting for another, no way to forcibly take a resource back, and a circle of threads each waiting for the next. Remove any one ingredient and deadlock becomes impossible. The prevention techniques below each remove one.
Walking through the example
t1 takes lock_a, sleeps briefly, then asks for lock_b. t2 does the opposite: it takes lock_b, sleeps, then asks for lock_a. The sleep gives each thread time to grab its first lock. Now t1 holds A and waits for B, while t2 holds B and waits for A. Neither can proceed, and the program hangs without any error message. That silence is what makes deadlocks hard to debug.
Fix 1: consistent lock ordering
If every thread takes A before B, a circle can never form. When locks are chosen dynamically, sort them by a fixed key, such as their id:
import threading
a = threading.Lock()
b = threading.Lock()
def transfer(first: threading.Lock, second: threading.Lock) -> None:
# Always lock in a global order, whatever order the caller used
low, high = sorted((first, second), key=id)
with low:
with high:
print("transfer done")
t1 = threading.Thread(target=transfer, args=(a, b))
t2 = threading.Thread(target=transfer, args=(b, a))
t1.start(); t2.start()
t1.join(); t2.join()Both threads end up locking in the same order, so this never deadlocks even though the arguments are swapped.
Fix 2: timeouts
lock.acquire(timeout=1) returns False instead of waiting forever. On failure you can release what you hold, wait a random moment, and try again. This turns a permanent hang into a recoverable delay, though ordering is a cleaner solution.
Fix 3: avoid holding more than one lock
If a thread only ever holds one lock at a time, deadlock is impossible. Copy the data you need inside the lock, release it, and do the slow work afterwards. Better still, replace shared state with a Queue.
Common mistakes
- Calling unknown code while holding a lock. A callback may take another lock and create a hidden cycle.
- Locking the same non-reentrant
Locktwice in one thread. That deadlocks with a single thread; useRLockwhen re-entry is genuine. - Assuming a hang was a "slow network". Check whether threads are waiting on locks.
- Holding a lock during I/O. Everyone else queues behind the slow call.
When to worry
Any program with two or more locks needs an ordering rule written down. If you can design the program so that threads communicate through queues instead of shared locked data, do that.
🧪 Try It Yourself
Exercise 1 – Basic Lock Usage
Tab indents. Press Escape, then Tab, to leave the editor. Control plus Enter runs the code; Control plus Shift plus Enter submits it.
Press Run to see what your code prints.Exercise 2 – Lock as Context Manager
Tab indents. Press Escape, then Tab, to leave the editor. Control plus Enter runs the code; Control plus Shift plus Enter submits it.
Press Run to see what your code prints.Exercise 3 – Detect Locked State
Tab indents. Press Escape, then Tab, to leave the editor. Control plus Enter runs the code; Control plus Shift plus Enter submits it.
Press Run to see what your code prints.