Deadlock¶
Four Coffman conditions (all must hold simultaneously): 1. Mutual exclusion: resource is non-shareable 2. Hold and wait: holding one resource while waiting for another 3. No preemption: resources cannot be forcibly taken 4. Circular wait: A waits for B, B waits for A
Prevention — break circular wait: Global lock ordering: always acquire Lock A before Lock B everywhere. Circular wait between A and B becomes impossible. This is the most practical strategy.
Break hold-and-wait: acquire all locks upfront atomically, or release all before acquiring new ones. Hard to implement generally.
Timeout: if lock acquisition times out, release held locks and retry. Prevents indefinite blocking but adds retry complexity.
Detection and recovery: PostgreSQL traverses the lock dependency graph after deadlock_timeout (1s default) and aborts the cheapest transaction to break the cycle.
Priority inversion: high-priority thread blocked on a lock held by a low-priority thread preempted by a medium-priority thread. Fix: priority inheritance — temporarily boost the lock holder's priority to match the highest waiter.
Common pitfall¶
Only breaking circular wait for the locks a developer remembers exist, while a third, less-obvious lock (a mutex buried inside a library call, or a database-level lock acquired indirectly through an ORM) participates in the same cycle, still deadlocks — global lock ordering only works if it's genuinely global, covering every lock a code path can acquire, not just the ones visible in the immediate function being written. This is exactly why the database-level version of this problem recommends sorting IDs before locking rather than "just remembering the right order" — a mechanical rule survives refactoring; a remembered convention doesn't.