Theme
Q049. How do deadlocks arise, and how can Rust code prevent them?
Short Answer
A deadlock occurs when participants wait forever for conditions that only one another can satisfy. The classic case is inconsistent lock order: one task holds A and waits for B while another holds B and waits for A. Rust prevents data races in safe code, but it does not prevent deadlocks. Use a global lock order, keep critical sections small, avoid awaiting or calling unknown code under a lock, and redesign toward one lock, partitioned ownership, or message passing where possible.
Deep Dive
The common necessary conditions are mutual exclusion, hold-and-wait, no forced preemption of a resource, and a circular wait. Breaking any condition prevents that deadlock class.
Practical controls include:
- acquire multiple locks in one documented global order;
- never recursively acquire a non-reentrant lock;
- do not retain map guards while invoking methods that may lock the same shard;
- use timeout or
try_lockonly as a recovery policy, not as proof of correctness; - avoid callbacks and
.awaitwhile holding locks; - model state so one owner performs related transitions.
The borrow checker tracks memory references, not dynamic wait-for graphs. A program can therefore be memory-safe and permanently stuck.
Internal Model
Contended locks place threads or tasks in wait queues. In a cycle, no owner reaches the unlock operation that would wake another waiter.
Production diagnosis often uses thread dumps, async task instrumentation, lock metrics, and timeout logs. Tests should force different operation orderings; normal happy-path tests may never reproduce the cycle.
Example
rust
use std::sync::Mutex;
struct State {
balances: Mutex<(i64, i64)>,
}
fn main() {
let state = State {
balances: Mutex::new((100, 100)),
};
let mut balances = state.balances.lock().unwrap();
balances.0 -= 10;
balances.1 += 10;
assert_eq!(*balances, (90, 110));
}Common Mistakes
- Claiming safe Rust cannot deadlock.
- Treating timeouts as a substitute for a consistent locking design.
- Acquiring locks inside helpers without documenting their lock-order requirements.
Follow-ups
- What is livelock, and how does it differ from deadlock?
- How would you diagnose an intermittent production deadlock?
- Can a single-threaded async task deadlock itself?
Related
- Q048. Why is holding a lock guard across
.awaitdangerous? - Q051. How do
DashMapreferences and guards create deadlock risks? - Q073. How do Tokio channels provide communication and backpressure?
References
- Rust Standard Library
- Tokio Documentation
- Linux kernel documentation