Introduction
When multiple threads need to read and write the same data, you need mutual exclusion. Rust's Mutex<T> wraps a value and ensures only one thread can access it at a time. Combined with Arc for shared ownership, Arc<Mutex<T>> is the standard pattern for shared mutable state across threads.
Key Concepts
- Mutex<T>: A mutual exclusion lock that wraps a value of type
T. Only one thread can hold the lock at a time. All access goes through the lock — there is no way to reach the inner value without locking. - MutexGuard<T>: The RAII guard returned by
lock(). It implementsDerefandDerefMut, giving you access to the inner value. The lock is released automatically when the guard is dropped. - Arc<Mutex<T>>:
Mutexitself is notClone. To share aMutexacross threads, wrap it inArc(Atomic Reference Counting). Each thread clones theArc, and all clones point to the sameMutex. - Poisoning: If a thread panics while holding a
MutexGuard, theMutexbecomes "poisoned". Subsequent calls tolock()returnErr(PoisonError), signaling that the protected data may be in an inconsistent state. - try_lock(): A non-blocking alternative to
lock(). ReturnsOk(guard)if the lock is available, orErr(TryLockError)if another thread holds it or the mutex is poisoned.
Real World Context
Shared counters, connection pools, caches, and configuration stores are all common use cases for Mutex. A web server might use Arc<Mutex<HashMap<String, Session>>> to store session data accessible from any request handler thread. Game engines use mutexes to protect shared world state that multiple systems (physics, rendering, AI) need to update.
Deep Dive
A Mutex guards its inner value by requiring you to call lock() before accessing it. The returned MutexGuard gives you &mut T access and automatically releases the lock when dropped.
rustuse std::sync::Mutex; let m = Mutex::new(5); { let mut guard = m.lock().unwrap(); *guard += 1; println!("value: {}", *guard); // 6 } // guard dropped here — lock released // Lock again to read println!("final: {}", *m.lock().unwrap()); // 6
To share a Mutex between threads, wrap it in Arc. Each thread gets its own Arc clone that points to the same underlying Mutex.
rustuse std::sync::{Arc, Mutex}; use std::thread; let counter = Arc::new(Mutex::new(0)); let mut handles = vec![]; for _ in 0..10 { let counter = Arc::clone(&counter); let handle = thread::spawn(move || { let mut num = counter.lock().unwrap(); *num += 1; }); handles.push(handle); } for handle in handles { handle.join().unwrap(); } println!("final count: {}", *counter.lock().unwrap()); // 10
The try_lock() method attempts to acquire the lock without blocking. This is useful when you want to do other work if the lock is not immediately available.
rustuse std::sync::Mutex; let m = Mutex::new(42); match m.try_lock() { Ok(guard) => println!("got the lock: {}", *guard), Err(_) => println!("lock is held by another thread, doing other work"), }
When a thread panics while holding a MutexGuard, the mutex becomes poisoned. This is a safety mechanism — the data inside might be in a half-updated, inconsistent state. You can choose to handle poisoning or propagate it.
rustuse std::sync::{Arc, Mutex}; use std::thread; let data = Arc::new(Mutex::new(vec![1, 2, 3])); let data_clone = Arc::clone(&data); let _ = thread::spawn(move || { let mut guard = data_clone.lock().unwrap(); guard.push(4); panic!("oops!"); // guard is held when panic occurs — mutex is poisoned }).join(); // Subsequent lock attempts return Err(PoisonError) match data.lock() { Ok(guard) => println!("data: {:?}", *guard), Err(poison_err) => { // Recover the data if you believe it's still valid let guard = poison_err.into_inner(); println!("recovered poisoned data: {:?}", *guard); } }
You can also clear the poisoned state so future callers do not see the error.
rustuse std::sync::Mutex; let m = Mutex::new(0); // ... after poisoning ... m.clear_poison(); // Now lock() returns Ok again
A common pattern is to hold the lock for the shortest time possible. Avoid doing I/O or expensive computation while holding a MutexGuard — other threads will block waiting for you.
rustuse std::sync::{Arc, Mutex}; let shared_config = Arc::new(Mutex::new(load_config())); // Good: lock briefly, clone the data, release immediately let config_snapshot = { let guard = shared_config.lock().unwrap(); guard.clone() }; // lock released here // Work with the snapshot without holding the lock process_config(&config_snapshot);
Common Pitfalls
- Deadlocks from nested locking — If a thread tries to lock a
Mutexit already holds, it will deadlock becauseMutexin Rust is not reentrant. Always ensure you drop the guard before locking the same mutex again. - Holding locks across
.awaitor long operations — Holding aMutexGuardacross an.awaitpoint in async code or during I/O starves other threads. Extract the data you need, drop the guard, then do the slow work.
Best Practices
- Minimize lock scope — Use a block
{ let guard = m.lock().unwrap(); ... }to ensure the guard is dropped as soon as possible. This reduces contention and deadlock risk. - Use
Mutex::new()with interior mutability in mind —Mutexprovides interior mutability: even if theMutexitself is behind a shared reference (&Mutex<T>), you get&mut Taccess through the guard. Design your types to take advantage of this.
Summary
Mutex<T>ensures only one thread accesses the inner value at a time via an RAII guard.Arc<Mutex<T>>is the standard pattern for sharing mutable state across threads.try_lock()provides non-blocking lock acquisition.- Poisoning protects against inconsistent state after a thread panic; use
PoisonError::into_inner()to recover if the data is still valid. - Keep lock scopes as short as possible to minimize contention.
Code Examples
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Final count: {}", *counter.lock().unwrap());
}