Arc<Mutex<T>>
TL;DR Arc<T> lets multiple owners share data across threads. Mutex<T> ensures only one thread accesses the inner T at a time. Together they're the go-to pattern for shared mutable state when you can't design around it.
Why Both Are Needed
Arc<T> → shared ownership (multiple threads hold a reference)
Mutex<T> → exclusive access (only one thread modifies at a time)
Rc<T> is the single-threaded version of Arc<T> — non-atomic, cheaper, but not Send.
Basic Usage
use std::sync::{Arc, Mutex};
use std::thread;
let counter = Arc::new(Mutex::new(0u64));
let handles: Vec<_> = (0..4).map(|_| {
let counter = Arc::clone(&counter);
thread::spawn(move || {
let mut n = counter.lock().unwrap();
*n += 1;
})
}).collect();
for h in handles {
h.join().unwrap();
}
println!("{}", counter.lock().unwrap()); // 4
Arc::clone increments the reference count atomically. When the last Arc is dropped, the inner value is freed.
Lock Acquisition
Mutex::lock() returns LockResult<MutexGuard<T>>. The MutexGuard releases the lock when dropped (RAII).
{
let mut guard = mutex.lock().unwrap();
*guard = 42;
} // lock released here
Mutex::try_lock() returns immediately with Err if the lock is held:
match mutex.try_lock() {
Ok(mut guard) => *guard += 1,
Err(_) => eprintln!("lock contended"),
}
Poisoning
If a thread panics while holding a lock, the mutex is poisoned. Subsequent lock() calls return Err(PoisonError).
let guard = mutex.lock().unwrap_or_else(|e| e.into_inner());
Call into_inner() on the PoisonError to recover the MutexGuard and inspect the data. Whether to recover or propagate depends on whether the data is in a consistent state.
RwLock for Read-Heavy Workloads
RwLock<T> allows many concurrent readers or one exclusive writer. Better than Mutex when reads dominate.
use std::sync::RwLock;
let data = Arc::new(RwLock::new(vec![1, 2, 3]));
// multiple concurrent readers
let r = data.read().unwrap();
println!("{:?}", *r);
drop(r);
// exclusive writer
let mut w = data.write().unwrap();
w.push(4);
Async Context: tokio::sync::Mutex
The standard Mutex is not async-aware: holding a std::sync::MutexGuard across an .await will either deadlock or fail to compile (because MutexGuard is not Send).
Use tokio::sync::Mutex when sharing state across async tasks:
use tokio::sync::Mutex;
let state = Arc::new(Mutex::new(HashMap::new()));
tokio::spawn(async move {
let mut map = state.lock().await; // async lock
map.insert("key", "value");
});
Alternatives
| Pattern | Use when |
|---|---|
Arc<Mutex<T>> | Shared mutable state, low contention |
Arc<RwLock<T>> | Shared state, reads >> writes |
tokio::sync::Mutex | Async tasks, lock held across .await |
DashMap | High-contention concurrent hashmap |
Message passing (mpsc) | Prefer ownership transfer over shared state |
Prefer message passing when possible — it's easier to reason about and avoids lock contention entirely.