Skip to main content

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

PatternUse when
Arc<Mutex<T>>Shared mutable state, low contention
Arc<RwLock<T>>Shared state, reads >> writes
tokio::sync::MutexAsync tasks, lock held across .await
DashMapHigh-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.

Ownership ModelWhy Rust needs explicit sharing mechanisms like Arc Async/AwaitWhy std::sync::Mutex is problematic in async contexts