Race Conditions, Locks, and Deadlocks
What goes wrong when threads share state — and how mutexes, semaphores, and atomics fix it.
The bug that doesn't reproduce
A race condition is a bug where the program's behavior depends on the (non-deterministic) order in which threads execute. They're maddening because they vanish under the debugger and reappear in production.
# Two threads, each running this loop, on a shared counter:
for _ in range(100_000):
counter += 1
# Final counter is NOT 200,000 — it's some random number less than that.
# Why? Because counter += 1 is THREE operations:
# 1. read counter into register
# 2. add 1
# 3. write back
# If thread A reads 5, thread B reads 5, both add 1, both write 6 — one increment is lost.Mutexes (mutual exclusion locks)
A mutex is an object you LOCK before entering a critical section and UNLOCK on the way out. Only one thread can hold the lock at a time; the rest wait. This serializes access to shared data.
from threading import Lock
lock = Lock()
counter = 0
def increment():
global counter
with lock: # acquire / release automatic
counter += 1Deadlock
Deadlock occurs when two or more threads are waiting on each other in a cycle. Classic recipe: thread A holds lock 1, wants lock 2; thread B holds lock 2, wants lock 1. Neither can proceed. Prevention: always acquire locks in a consistent global order; or use try-acquire-with-timeout.
Other primitives
- Semaphore — counter that allows up to N concurrent holders. Used for connection pools.
- Read-write lock — many readers OR one writer. Faster when reads dominate.
- Condition variable — thread waits until some predicate becomes true.
- Atomic operations — single-instruction read-modify-write, no lock needed (e.g. atomic counters).