Poisoned Mutexes
A Mutex<T> (or RwLock<T>) becomes poisoned when a thread panics while holding an exclusive write guard (MutexGuard<'_, T> or RwLockWriteGuard<'_, T>). During stack unwinding, the guard's Drop implementation still unlocks the underlying OS lock (so waiting threads do not deadlock forever), and sets the lock's internal poison flag to true to warn future callers that the inner data T may have been left in a half-updated state.
How Poisoning Happens and How Callers Respond
Thread 1 (Holds `MutexGuard<'_, BankLedger>`)
+-- Deducts $100 from `savings`
+-- PANICS before adding $100 to `checking`!
+-- Unwinding runs `MutexGuard::drop`:
1. Sets `mutex.poison = true`
2. Unlocks the OS futex
Thread 2 (Calls `mutex.lock()`)
+-- Acquires the OS futex
+-- Sees `mutex.poison == true`
+-- Returns `Err(PoisonError<MutexGuard<'_, BankLedger>>)`!
When .lock() returns Err(poison_err), you have three ways to handle it:
| Strategy | Code Idiom | When to Use |
|---|---|---|
| 1. Propagate the panic | let guard = mutex.lock().unwrap(); |
Default in most applications: if one worker thread panicked while mutating shared state, fail fast |
2. Recover the guard (into_inner) |
let guard = mutex.lock().unwrap_or_else(|e| e.into_inner()); |
When every mutation before a potential panic leaves T in a valid state, or when inspecting logs during shutdown |
3. Repair state and .clear_poison() |
Repair T via e.into_inner(), then call mutex.clear_poison(); |
Self-healing supervisor that resets broken state so subsequent .lock() calls return Ok(guard) again |
Repairing a Poisoned Mutex<T> and Clearing the Poison Flag
Stabilized in Rust 1.77, Mutex::clear_poison(&self) (and RwLock::clear_poison(&self)) resets the poison flag back to false after you have restored the inner value's invariants:
use std::sync::Mutex;
use std::thread;
#[derive(Debug)]
struct TransferState {
source_balance: i64,
dest_balance: i64,
in_flight: bool,
}
fn main() {
let ledger = Mutex::new(TransferState {
source_balance: 500,
dest_balance: 500,
in_flight: false,
});
// A worker panics mid-transfer while `in_flight` is true:
let _ = thread::scope(|s| {
s.spawn(|| {
let mut guard = ledger.lock().unwrap();
guard.in_flight = true;
guard.source_balance -= 100;
panic!("network disconnect before crediting dest_balance");
})
.join()
});
println!("is_poisoned after worker panic: {}", ledger.is_poisoned());
// Supervisor recovers the guard, rolls back the broken transaction, and clears poison:
if let Err(poison_err) = ledger.lock() {
let mut guard = poison_err.into_inner();
if guard.in_flight {
println!("repairing half-completed transfer...");
guard.source_balance += 100;
guard.in_flight = false;
}
drop(guard);
ledger.clear_poison();
}
println!("is_poisoned after repair: {}", ledger.is_poisoned());
println!("clean lock state: {:?}", *ledger.lock().unwrap());
}
is_poisoned after worker panic: true
repairing half-completed transfer...
is_poisoned after repair: false
clean lock state: TransferState { source_balance: 500, dest_balance: 500, in_flight: false }
Designing Panic-Safe Critical Sections (Preventing Broken Invariants)
To ensure that even if a panic occurs while a MutexGuard is held the inner data T never violates its invariants, perform all fallible or potentially panicking work before modifying T, and then commit the state change at the end of the critical section using non-panicking assignments or std::mem::replace.