How to Use Arc<Mutex> Pattern in Rust
You are building a chat server. Ten threads are processing incoming messages. Every time a user joins, you need to increment a global user count and add their name to a shared list. You try to pass a mutable reference to the threads, and the compiler rejects you. You try to clone the data, but then every thread works on a stale copy and the updates vanish. You need a way to hand out copies of the data that all point to the same place, but only let one thread touch it at a time. That is the Arc<Mutex<T>> pattern.
Rust's ownership rules forbid multiple mutable references. Threads complicate this further because the compiler cannot always prove at compile time which thread accesses data when. Arc<Mutex<T>> solves this by combining two tools: Arc for shared ownership across threads, and Mutex for runtime synchronization.
The two halves of the pattern
Arc stands for Atomic Reference Counting. It is the thread-safe version of Rc. It wraps a value on the heap and maintains a counter of how many Arc handles exist. When you clone an Arc, the counter bumps. When an Arc drops, the counter decrements. When the counter hits zero, the value is destroyed. Arc is Send and Sync, meaning you can move it across threads and share it safely.
Mutex stands for Mutual Exclusion. It is a lock. It wraps a value and ensures that only one thread can access the value at a time. To read or write, a thread must acquire the lock. If another thread holds the lock, the current thread blocks until the lock is released. The lock is released when the guard object returned by lock() goes out of scope.
Think of a shared whiteboard in a breakroom. Arc is the mechanism that keeps the whiteboard from disappearing while someone is looking at it. Mutex is the single marker on the wall. You have to grab the marker to write. If someone else has it, you wait. When you are done, you put the marker back. Arc keeps the data alive. Mutex keeps the data consistent. You usually need both.
Minimal example
Here is the canonical counter. Ten threads increment a shared integer.
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
// Wrap the value in a Mutex for synchronization, then in an Arc for shared ownership.
// The order matters: Arc<Mutex<T>> allows multiple owners of a single lock.
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
// Clone the Arc, not the inner value. This bumps the reference count.
// Convention: use Arc::clone(&x) instead of x.clone() to signal
// that this is a shallow clone of the pointer, not a deep copy.
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
// Lock the mutex to get exclusive access.
// lock() returns a Result. unwrap() panics if the mutex is poisoned.
// A poisoned mutex means another thread panicked while holding the lock.
let mut num = counter.lock().unwrap();
// num is a MutexGuard. It dereferences to the inner value.
// The lock is held as long as num is in scope.
*num += 1;
// num goes out of scope here. The guard drops, releasing the lock.
// Other threads can now acquire the lock.
});
handles.push(handle);
}
// Wait for all threads to finish.
for handle in handles {
handle.join().unwrap();
}
// Lock one last time to read the result.
println!("Result: {}", *counter.lock().unwrap());
}
Walkthrough: what happens at runtime
When main starts, Arc::new(Mutex::new(0)) allocates a Mutex containing 0 on the heap. The Arc holds a pointer to that allocation and a reference count of one.
The loop runs ten times. Each iteration calls Arc::clone(&counter). This does not copy the integer or the mutex. It increments the reference count and returns a new Arc pointing to the same heap allocation. The move closure captures this Arc by value. The thread now owns one handle to the shared data.
Inside the thread, counter.lock() attempts to acquire the lock. If no one holds it, the thread proceeds immediately. If another thread holds it, this thread blocks. The OS puts the thread to sleep until the lock becomes available. Once acquired, lock() returns a MutexGuard. This guard implements Deref, so you can treat it like a &mut T. You modify the value.
When the closure ends, num drops. The MutexGuard destructor releases the lock. Any waiting thread wakes up and can try to acquire the lock.
After join, all threads have finished. The reference count drops to one in main. You lock again to read the value. When main ends, the last Arc drops, the count hits zero, and the heap allocation is freed.
Keep the lock scope tight. The moment you unlock, other threads can proceed. Holding the lock longer than necessary increases contention and slows down the whole program.
Realistic example: shared state struct
In real code, you rarely share a single integer. You share a struct with multiple fields. Arc<Mutex<T>> shines here because you can update multiple fields atomically.
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::thread;
struct ServerState {
users: HashMap<String, u32>,
total_messages: u64,
}
fn main() {
// Initialize shared state.
let state = Arc::new(Mutex::new(ServerState {
users: HashMap::new(),
total_messages: 0,
}));
let mut handles = vec![];
// Simulate five worker threads processing messages.
for id in 0..5 {
let state = Arc::clone(&state);
handles.push(thread::spawn(move || {
// Simulate receiving a message from a user.
let user = format!("user_{}", id);
// Lock the state to update both the user map and the counter.
// Both updates happen atomically relative to other threads.
let mut guard = state.lock().unwrap();
// Update user message count.
guard.users.entry(user).and_modify(|c| *c += 1).or_insert(1);
// Update global counter.
guard.total_messages += 1;
// guard drops here, releasing the lock.
}));
}
for h in handles {
h.join().unwrap();
}
// Read the final state.
let final_state = state.lock().unwrap();
println!("Total messages: {}", final_state.total_messages);
for (user, count) in &final_state.users {
println!("{} sent {} messages", user, count);
}
}
This pattern ensures that total_messages and users stay in sync. If you used separate Mutex wrappers for each field, you would risk a thread reading total_messages before the users map is updated. Wrapping the whole struct in one Mutex guarantees consistency.
Pitfalls and compiler errors
The compiler protects you from data races, but runtime issues remain.
If you try to share an Rc across threads, the compiler rejects you with E0277 (trait bound not satisfied). Rc is not Send because its reference counting is not atomic. Arc fixes this by using atomic operations for the counter. Always use Arc when crossing thread boundaries.
Deadlocks happen when two threads wait for each other. Thread A holds lock 1 and wants lock 2. Thread B holds lock 2 and wants lock 1. Neither can proceed. Rust cannot prevent deadlocks at compile time. You must establish a consistent lock ordering or use tools like try_lock to detect cycles.
Poisoned mutexes occur when a thread panics while holding a lock. The mutex marks itself as poisoned to signal that the data might be inconsistent. Subsequent calls to lock() return an error. If you use unwrap(), your program panics too. This is often the desired behavior: if one thread corrupted the state, crashing is safer than continuing with garbage data. If you need to recover, handle the error and extract the inner value.
// Recover from a poisoned mutex if the data is still usable.
let guard = state.lock().unwrap_or_else(|e| e.into_inner());
Use this recovery pattern only if you can prove the data is valid despite the panic. Otherwise, let the panic propagate.
Convention aside: never hold a mutex while doing I/O or heavy computation. If you lock, do the minimal work, and unlock. Holding a lock during a network request blocks every other thread that needs the data. Your server becomes a single-threaded bottleneck.
Trust the borrow checker for compile-time safety, but respect the runtime lock for correctness. A deadlock stops your program dead.
When to use this pattern
Use Arc<Mutex<T>> when you need shared mutable state across threads and the updates involve multiple fields or complex logic. Use atomic types like AtomicI32 when you only need to update a single primitive value and want lock-free performance. Use channels like mpsc or crossbeam when threads send data to each other rather than sharing a common bucket. Use Rc<RefCell<T>> when you need shared mutable state but only within a single thread.
Locks are expensive. Measure contention before you reach for them. If your threads spend more time waiting for locks than doing work, reconsider your design. Split the state, use fine-grained locks, or switch to message passing.