155. Mutex<T> — Cross-Thread Exclusive Access, With a Guard Instead of a Panic
Yesterday’s RefCell<T> gives you &mut through &self on a single thread — and panics the moment a second borrow shows up. Mutex<T> is the same idea wearing a hard hat: it’s Sync, so it works across threads, and instead of panicking on contention it just blocks until the other holder is done.
The pain: RefCell is single-threaded, and panics on contention
RefCell<T> is !Sync. The moment you try to share one across threads, the compiler stops you:
| |
Even on one thread, RefCell will panic if a borrow_mut() clashes with a live borrow(). That’s fine for logic bugs you want to find loudly — useless for two real threads that genuinely both want to write.
The fix: lock() returns a guard
Mutex<T> is Sync (when T: Send), and its .lock() method takes &self, blocks until the mutex is free, and hands back a MutexGuard<'_, T>. The guard derefs to &mut T, and releases the lock when it drops:
| |
The .unwrap() is there because lock() returns Result — see “Poisoning” below.
The classic: Arc<Mutex<T>> across threads
Mutex is the inside half. To share ownership across threads you wrap it in Arc — the thread-safe sibling of Rc (covered in Sunday’s morning bite):
| |
Eight threads, a hundred increments each, eight hundred total — no torn writes, no races, because every increment runs while exactly one thread holds the lock.
The footgun: holding the guard too long
The single most common Mutex bug is leaving the guard alive across slow work. Anything between lock() and the guard going out of scope is serialized:
| |
drop(g) works too if you can’t easily restructure. The mental model: the guard is a lease, keep it as short as you can.
Poisoning, and why .lock() returns Result
If a thread panics while holding the lock, the Mutex is poisoned. Future lock() calls return Err(PoisonError) so you can decide whether the data is still consistent. You can always recover the inner guard with .into_inner():
| |
For data where one panic mid-update really would leave things half-written, the Err is the signal to crash the whole component instead of papering over it.
When to pick Mutex vs RefCell vs RwLock
RefCell<T> for single-threaded interior mutability where contention is a bug. Panics loudly. Zero locking cost.
Mutex<T> when more than one thread needs to write — or might. Blocks instead of panicking. One holder at a time, readers and writers treated identically.
RwLock<T> (covered in this afternoon’s bite) when reads vastly outnumber writes and you want many readers to proceed in parallel. Pricier per-op than Mutex, but the parallelism wins for read-heavy workloads.