Deadlock Prevention
A deadlock occurs when one or more threads block permanently waiting for locks or channel events that can never be released. Because deadlocks are logical liveness failures rather than memory-safety violations, preventing them requires disciplined lock-acquisition architecture.
The Four Necessary Conditions for Deadlock (Coffman Conditions)
A deadlock can occur only if all four of the following conditions hold simultaneously; breaking any single one makes deadlock impossible:
| # | Condition | How to Break It in Rust |
|---|---|---|
| 1 | Mutual Exclusion | Use lock-free atomics (AtomicU64), read-only sharing (Arc<T>), or thread-local storage (thread_local!) |
| 2 | Hold and Wait (holding Lock A while blocking to acquire Lock B) | Release Lock A (drop(guard_a)) before acquiring Lock B, or coalesce both fields into a single Mutex<(A, B)> |
| 3 | No Preemption (locks cannot be forcibly taken away) | Use non-blocking .try_lock(); if acquiring Lock B fails with WouldBlock, drop Lock A and retry after a brief backoff |
| 4 | Circular Wait (Thread 1 waits on Thread 2; Thread 2 waits on Thread 1) | Acquire multiple locks in a strictly increasing global lock order (by rank, ID, or memory address) |
Breaking Circular Wait via Lock Hierarchy (Level 1 -> Level 2 -> Level 3):
[ Level 1: Global Router Lock ]
|
v (Allowed direction: Low Level -> High Level ONLY)
[ Level 2: Session Table Lock ]
|
v
[ Level 3: Metrics Sink Lock ]
(No code holding a Level 3 lock may ever attempt to acquire a Level 1 or Level 2 lock!)
Strategy 1: Coalescing Related Fields into a Single Mutex<State>
When two fields (users and roles) are frequently updated together, putting them in two separate Mutexes (users: Mutex<...>, roles: Mutex<...>) invites AB-BA deadlocks and pays double the locking cost. Grouping both fields inside a single struct AuthState { users: ..., roles: ... } wrapped in one Mutex<AuthState> eliminates multi-lock ordering bugs completely.
Strategy 2: Non-Blocking .try_lock() Backoff Loop
When a global lock order cannot be determined statically, using .try_lock() on the second lock and releasing the first lock if .try_lock() returns Err(TryLockError::WouldBlock) breaks the Hold and Wait condition:
use std::sync::Mutex;
use std::thread;
struct BoxedSlot {
name: &'static str,
data: Mutex<i32>,
}
fn swap_slots_nonblocking(first: &BoxedSlot, second: &BoxedSlot) {
if std::ptr::eq(first, second) {
return;
}
loop {
let mut guard_1 = first.data.lock().unwrap();
// Attempt to acquire `second` without blocking while holding `guard_1`:
if let Ok(mut guard_2) = second.data.try_lock() {
std::mem::swap(&mut *guard_1, &mut *guard_2);
return;
}
// `second` was busy! Drop `guard_1` immediately so another thread holding
// `second` can acquire `first` and finish:
drop(guard_1);
thread::yield_now();
}
}
fn main() {
let slot_a = BoxedSlot {
name: "A",
data: Mutex::new(10),
};
let slot_b = BoxedSlot {
name: "B",
data: Mutex::new(99),
};
thread::scope(|s| {
s.spawn(|| swap_slots_nonblocking(&slot_a, &slot_b));
s.spawn(|| swap_slots_nonblocking(&slot_b, &slot_a));
});
println!(
"slot {}: {}, slot {}: {}",
slot_a.name,
*slot_a.data.lock().unwrap(),
slot_b.name,
*slot_b.data.lock().unwrap()
);
}
slot A: 10, slot B: 99
Strategy 3: Never Invoke User Callbacks While Holding a Lock
Calling an external closure callback(&*guard) while holding a MutexGuard risks a same-thread re-entrancy deadlock if callback calls back into your struct and tries to lock the same Mutex. Copy or clone the event payload, drop(guard) first, and then invoke callback outside the lock.