Arc<Mutex<T>> in Rust: sharing mutable state between threads
How to share and change data across threads in Rust with Arc and Mutex, how lock guards work, poisoning, RwLock, atomics, and avoiding deadlocks.
When several threads need to change the same data, Rust's usual tools don't work: ownership allows only one owner, &mut allows only one writer, and Rc/RefCell aren't thread-safe. The standard answer is Arc<Mutex<T>>:
Arc<T>(atomically reference counted) gives every thread an owning handle to the same data.Arc::clone(&a)bumps a counter (atomically, so it's safe across threads); the data is freed when the last handle is dropped.Mutex<T>(mutual exclusion) guarantees only one thread at a time accesses theTinside.
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..8 {
let counter = Arc::clone(&counter);
handles.push(thread::spawn(move || {
for _ in 0..1000 {
*counter.lock().unwrap() += 1;
}
}));
}
for h in handles {
h.join().unwrap();
}
println!("count = {}", *counter.lock().unwrap());
}
count = 8000
How lock() works
mutex.lock() waits until no other thread holds the lock, then returns a MutexGuard. The guard derefs to the data (*guard += 1, guard.push(x)), and unlocks automatically when it's dropped, at the end of the statement or block. You can't forget to unlock, and you can't touch the data without locking: it's inside the mutex.
lock() returns a Result because of poisoning: if a thread panicked while holding the lock, the data might be half-updated, and later lock() calls return Err. Most code calls .unwrap() (propagating the panic) or .unwrap_or_else(|e| e.into_inner()) to accept the data anyway.
Why not Rc<Mutex>?
use std::rc::Rc;
use std::sync::Mutex;
use std::thread;
fn main() {
let counter = Rc::new(Mutex::new(0));
let c = Rc::clone(&counter);
let h = thread::spawn(move || {
*c.lock().unwrap() += 1;
});
h.join().unwrap();
}
error[E0277]: `Rc<std::sync::Mutex<i32>>` cannot be sent between threads safely
Rc's counter isn't atomic, so two threads cloning it at once could corrupt the count. The compiler knows this through the Send trait. Swap Rc for Arc.
Alternatives
| Tool | Use when |
|---|---|
Mutex<T> |
general shared mutable state |
RwLock<T> |
many readers, rare writers (read() / write()) |
AtomicUsize, AtomicBool, ... |
a single counter or flag: fetch_add(1, Ordering::Relaxed), no lock |
channels (mpsc) |
threads hand results to one owner instead of sharing |
thread::scope + &mut chunks |
each thread works on its own part of the data |
OnceLock / LazyLock |
a global initialized once, then read-only |
Common mistakes
- Holding a lock too long. Keep the guard's scope tiny: compute outside, lock, update, drop. A guard held across slow work serializes your threads.
- Deadlock from locking twice.
let a = m.lock().unwrap(); let b = m.lock().unwrap();on the same mutex in one thread blocks forever. Two mutexes locked in opposite orders by two threads also deadlock; always lock in a consistent order. - A guard living longer than you think. In
if let Some(x) = map.lock().unwrap().get(k) { ... }, the guard lives for the wholeif letblock. Copy what you need into a local first. - Holding a
std::sync::MutexGuardacross.awaitin async code: the future stops beingSend. Drop the guard before awaiting, or usetokio::sync::Mutex.
Example
use std::collections::BTreeMap;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex, RwLock};
use std::thread;
fn main() {
let orders = vec![("north", 120), ("south", 75), ("north", 30), ("east", 210), ("south", 25), ("east", 5)];
let totals: Arc<Mutex<BTreeMap<&str, u32>>> = Arc::new(Mutex::new(BTreeMap::new()));
let processed = Arc::new(AtomicUsize::new(0));
let config = Arc::new(RwLock::new(10_u32)); // shipping fee, read by all
let mut handles = Vec::new();
for chunk in orders.chunks(2) {
let chunk = chunk.to_vec();
let totals = Arc::clone(&totals);
let processed = Arc::clone(&processed);
let config = Arc::clone(&config);
handles.push(thread::spawn(move || {
let fee = *config.read().unwrap();
for (region, amount) in chunk {
let with_fee = amount + fee; // compute outside the lock
*totals.lock().unwrap().entry(region).or_insert(0) += with_fee;
processed.fetch_add(1, Ordering::Relaxed);
}
}));
}
for h in handles {
h.join().unwrap();
}
*config.write().unwrap() = 0; // writers get exclusive access
println!("processed {} orders", processed.load(Ordering::Relaxed));
for (region, total) in totals.lock().unwrap().iter() {
println!("{region:<6} {total}");
}
println!("fee is now {}", config.read().unwrap());
}
Output:
processed 6 orders
east 235
north 170
south 120
fee is now 0