Skip to content

← Fearless Concurrency: Threads, Channels, Shared State step 13 of 24

Medium Framework

Poisoning: what a panic does to a lock

Apply a list of operations to a shared counter, one thread per op, and report the state of the lock after each.

pub fn poison_report(ops: Vec<i64>) -> Vec<String>

The counter is an Arc<Mutex<i64>> starting at 0. For each op, in order: spawn a thread, join it, then report.

The worker locks the mutex, adds op to the counter, and — if op is negative — panics while still holding the guard.

The report, one entry per op:

  • lock healthy → "ok:<value>";
  • lock poisoned → recover the value, clear the poison, and push "poisoned:<value>".
[1, 2, -1, 3]  ->  ["ok:1", "ok:3", "poisoned:2", "ok:5"]
[-5, 2]        ->  ["poisoned:-5", "ok:-3"]

Note the negative op’s contribution is in the total: the worker updated the counter and then panicked. That is exactly the situation poisoning exists to warn you about, and here it happens to be harmless.

Spawn-and-join each op before starting the next. Yes, that makes the concurrency purely illustrative — the point is that a panicking thread’s effect on the lock is observable and deterministic, and overlapping the ops would make the poisoning point scheduler-dependent.

Why lock() returns a Result

You have written .unwrap() after every lock() in this track. Here is what it is for.

If a thread panics while holding a MutexGuard, unwinding drops the guard and releases the lock — but the data may be halfway through an update. Half a transaction applied. An invariant temporarily broken and never restored.

Mutex records that fact. Every subsequent lock() returns Err(PoisonError<MutexGuard<T>>), forever, until someone clears it. The message is not “you cannot have this data”; it is “this data may be inconsistent, and someone should decide what to do about that.”

So .unwrap() after lock() is a real decision: if a previous holder panicked, take this thread down too. Often that is the right call. It is still a decision, and this problem asks you to make the other one.

Recovering

The error carries the guard, so the data is not lost:

match shared.lock() {
    Ok(guard) => { /* healthy */ }
    Err(poisoned) => {
        let guard = poisoned.into_inner();   // the MutexGuard, poison and all
        // ... inspect or repair the value ...
        shared.clear_poison();               // stable since 1.77
    }
}

PoisonError::into_inner() hands you the guard. Mutex::clear_poison() resets the flag so later lock() calls succeed again. Together they are “I have looked at the damage and I accept responsibility for it”.

Mind the guard’s lifetime: read the value out and let the guard drop before you call clear_poison, or you are holding the lock while asking the mutex to do something.

The rules for the rest of the family

type poisons?
Mutex<T> yes, on any panic while the guard is held
RwLock<T> only on a writer panic. A reader panic cannot break an invariant, because readers cannot write.
OnceLock<T> never
LazyLock<T, F> yes, and irrecoverably — a panic in the initialiser leaves it permanently unusable

That OnceLock/LazyLock asymmetry catches people out; they look like siblings and behave nothing alike under panic.

Poisoning is advisory, and contested

Be precise about what the guarantee is worth. The std docs say poisoning is advisory: it can be missed during a double panic, and unsafe code must never rely on it for soundness. It is a debugging aid with a strong opinion, not a safety mechanism.

And the design is genuinely disputed. The cost is .unwrap() on every single lock in every program, which trains people to ignore the Result — the exact opposite of what error handling is for. parking_lot::Mutex has no poisoning at all, and that is the most-cited reason people leave std for it.

The direction of travel is public: std::sync::nonpoison exists on nightly (sync_nonpoison, tracking issue #134645), and there is an open proposal (rust-lang/rust#149359) to make non-poisoning locks the Edition 2027 default. Learn the mechanism, and know it is a live argument rather than settled law.

Loading visualization…