Common Pitfalls and Deadlocks
Concurrency bugs are mostly a small set of recurring patterns: deadlocks from lock ordering, starvation from holding locks too long, uncaught thread exceptions, and - in asyncio - the single worst bug of all, a blocking call inside the loop. This chapter catalogs them with the fix for each.
Deadlocks
Two threads each hold a lock and wait for the other's: neither can proceed. The classic shape is inconsistent lock ordering:
import threading
lock_a = threading.Lock()
lock_b = threading.Lock()
def t1():
with lock_a:
with lock_b: # A then B
pass
def t2():
with lock_b:
with lock_a: # B then A - deadlock with t1
pass
Fixes, in order of preference: eliminate the second lock (one lock per shared object, owned by that object), impose a global lock ordering and always acquire in that order, or use non-blocking acquisition with a retry/fallback. If a deadlock is already in production, faulthandler.dump_traceback() on a stuck process shows exactly which threads hold which locks.
Other Lock Mistakes
- Self-deadlock: a plain
Lockacquired twice by the same thread (fixed byRLockor restructuring). - Forgotten release: an exception between
acquire()andrelease()- always usewithortry/finally. - Lock around I/O: sleeping, socket, or
join()inside a critical section serializes everything behind it. - Copy-pasted lock scope: the locked region is smaller (or bigger) than the invariant actually requires.
Silent Thread Deaths
An uncaught exception in a thread prints a traceback and kills only that thread; the main program continues with half-initialized state. Catch inside the target and record the error (the Worker.error pattern from the thread-creation chapter), or route work through concurrent.futures where exceptions surface via future.result().
asyncio Pitfalls
- Blocking the loop:
time.sleep,requests.get, synchronous DB calls freeze all tasks. Replace with async equivalents orasyncio.to_thread. - Forgetting await:
session.get(url)withoutawait/async withcreates a coroutine that never runs (and a RuntimeWarning if it is GC'd). - One session per request: creating a
ClientSessionper call loses the connection pool and can exhaust file descriptors; create one per program or worker. - Unbounded tasks: spawning one task per URL without a semaphore can open thousands of connections and crash the target; cap in-flight work.
- Swallowing CancelledError:
except Exceptiondoes not catch it (good), butexcept BaseException: passbreaks cancellation semantics - re-raise it.
Debugging Tools
python -X dev your_app.py # development mode: more warnings
python -m faulthandler -t # dump thread stack traces after 30s stall
pip install aiodebug # asyncio loop error tracking
Summary
Order your locks (or better, reduce the number of locks), release with with, make thread exceptions visible, and in asyncio never block the loop, always await, and bound task fan-out. When in doubt, dump thread stacks - the deadlock is usually staring at you in two frames.