Shared State with Mutex

Mutex<T> protects shared data by allowing only one thread at a time to access T, returning a guard that unlocks automatically when dropped.

What it is

A mutex is mutual exclusion around a value. Before reading or mutating the protected data, a thread must acquire the lock. Rust makes that discipline part of the type: you cannot access the inner T without obtaining a MutexGuard<T>.

Mutex<T> is also an interior-mutability primitive. An immutable &Mutex<T> can yield mutable access to T, but only while the runtime lock is held. That is why Mutex<T> is the thread-safe cousin of RefCell<T> rather than a replacement for ordinary &mut T.

How it works

Mutex::new(value) constructs the lock and protected value. lock() blocks until the lock is available and returns LockResult<MutexGuard<'_, T>>. The guard dereferences to T and releases the lock in its Drop implementation.

The LockResult exists because std mutexes are poisoned. If a thread panics while holding the lock, later lock() calls return Err(PoisonError<_>) as a warning that the protected data may be logically inconsistent. In many applications, .lock().unwrap() is a reasonable default because it propagates the corruption signal. Poisoning is advisory, not a soundness boundary for unsafe code: some panic paths and foreign exceptions may not poison, so memory safety must not depend on poisoning firing. Mutex::new is const on stable Rust, so a plain static Mutex<T> is possible when T can be built in a constant context.

Example

use std::sync::{Arc, Mutex};
use std::thread;
 
fn main() {
    let counter = Arc::new(Mutex::new(0));
    let mut handles = Vec::new();
 
    for _ in 0..4 {
        let counter = Arc::clone(&counter);
        handles.push(thread::spawn(move || {
            let mut guard = counter.lock().expect("counter mutex poisoned");
            *guard += 1;
        }));
    }
 
    for handle in handles {
        handle.join().unwrap();
    }
 
    println!("count = {}", *counter.lock().unwrap());
}

Example: recovering from poison deliberately

use std::sync::{Arc, Mutex};
use std::thread;
 
fn main() {
    let state = Arc::new(Mutex::new(vec![1, 2, 3]));
    let worker_state = Arc::clone(&state);
 
    let _ = thread::spawn(move || {
        let mut guard = worker_state.lock().unwrap();
        guard.push(4);
        panic!("worker failed after updating state");
    })
    .join();
 
    let mut guard = match state.lock() {
        Ok(guard) => guard,
        Err(poisoned) => {
            let mut guard = poisoned.into_inner();
            guard.clear();
            guard
        }
    };
 
    guard.push(10);
    println!("{guard:?}");
}

Common errors

Moving a bare Mutex<T> into multiple spawned closures fails because the first move consumes it:

error[E0382]: borrow of moved value: `counter`

The fix is not Rc<Mutex<T>>; Rc cannot cross threads. Use Arc Mutex Shared State, Channels, or Scoped Threads depending on whether ownership is shared, transferred, or only borrowed temporarily.

Best practice

  • ✅ Keep critical sections small: lock, inspect or mutate, then let the guard drop.
  • ✅ Put data that must stay consistent together behind one mutex instead of scattering invariants across several locks.
  • ✅ Treat poisoning as useful information; recover with into_inner only when you have a real recovery story.
  • ✅ Prefer Atomics for simple counters and flags, and RwLock for genuinely read-heavy shared state.
  • ✅ Use an inner block or explicit drop(guard) before waits, joins, callbacks, or expensive follow-up work.

Pitfalls

  • ⚠️ Holding a guard across slow work, callbacks, I/O, or .await creates Holding Locks Too Long.
  • ⚠️ Taking multiple mutexes without a consistent order creates Lock Order Reversal.
  • ⚠️ Wrapping everything in Arc<Mutex<_>> by default can be Premature Arc Mutex.
  • ⚠️ Returning MutexGuard from public APIs hides lock lifetime from the caller and makes Deadlock Avoidance harder.
  • ⚠️ Using poisoning as an unsafe code invariant is unsound; poisoning is a diagnostic signal, not a guarantee.

See also

Concurrency · Arc · Arc Mutex Shared State · RwLock · Atomics · Deadlock Avoidance · Holding Locks Too Long · Shared State in Async

Sources