Skip to content

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

Medium Framework

Poisoning: what a panic does to a lock

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

Apply operations to an Arc<Mutex<i64>> counter starting at zero. For each operation, spawn one worker and join it before starting the next. The worker locks the counter, adds the operation, and panics while holding the guard if the operation is negative. Repair the reporting code so these expected worker panics do not stop the function.

Required reports

After joining each worker, acquire the mutex and append one entry:

  • A healthy lock produces "ok:<value>".
  • A poisoned lock produces "poisoned:<value>". Recover the guard from the error, read the counter and clear the poison before the next worker starts.

Preserve every update, including negative operations. Do not reset or undo the counter when recovering. An empty input returns an empty list. Zero does not trigger a panic. Assume all running totals fit in i64; overflow is not part of this exercise's behavior.

[1, 2, -1, 3] -> ["ok:1", "ok:3", "poisoned:2", "ok:5"]
[-5, 2]       -> ["poisoned:-5", "ok:-3"]
[-1, -1]      -> ["poisoned:-1", "poisoned:-2"]

The label depends on the current operation, not whether the total is negative. In the second example, adding 2 leaves a negative total but a healthy lock. Joining each worker before continuing makes the update and report order deterministic. This exercise demonstrates thread failure and recovery rather than parallel throughput.

What poison means

In this worker, panic unwinding drops the guard, releases the mutex and marks it poisoned. A later lock() still acquires the lock but returns a PoisonError containing the guard. The flag warns that a previous operation may have left the protected data inconsistent.

The counter update here is deliberately complete before the panic, so the reporting thread can accept the recorded value. In a real application, inspect or repair the protected invariant before clearing poison. A poisoned flag does not identify which data is wrong, and clearing it does not repair data.

Recover the guard and clear the flag

PoisonError::into_inner() returns the acquired guard. It does not clear the poisoned flag. Mutex::clear_poison() clears that flag and does not acquire the mutex itself, so it can be called while you still hold the guard. Keeping the guard during inspection or repair prevents another thread from observing an intermediate state.

The reference copies this scalar counter, releases the temporary guard, then clears poison. That is sufficient here because the worker has been joined and no next worker has started. It is not a general requirement to unlock first. See the Mutex recovery example.

Treat the expected Err from handle.join() separately from the result of acquiring the mutex. Ignoring this particular join error allows reporting to continue; using unwrap() on it would panic in the reporting thread. The starter already joins each worker but incorrectly calls expect on the potentially poisoned reporting lock.

Limits of this demonstration

The exercise relies on panic unwinding. With aborting panics the process would terminate instead of returning a join error. The default panic hook may print diagnostics for negative operations; the returned report is still the result being compared.

Poisoning is advisory and does not detect every possible invariant violation or panic path. Unsafe code must not depend on it for memory safety. Other synchronization types can have different poisoning behavior; apply the contract of the particular type rather than assuming every guard behaves like a MutexGuard.