Operating Systems25 min readAdvanced

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 += 1

Deadlock

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).
⚠ Watch out
The simplest, fastest concurrency strategy is: don't share state. Use message passing (queues, channels) and copy data instead. This is the entire philosophy of Go and Erlang.