Rust Patterns That Matter #19: Arc<Mutex<T>> vs Arc<RwLock<T>>
Post 19 of 22 in Rust Patterns That Matter. Companion series: Building a Chat Server in Rust.
Previous: #18: Typestate | Next: #20: Channels
You have shared state and multiple threads. You reach for
Rc<RefCell<T>> because that's what worked in
#6. The compiler says no:
Rc is not Send, RefCell is not
Sync. The thread-safe equivalents are Arc and
Mutex. The shapes are identical; the guarantees are stronger.
The motivation
use std::rc::Rc;
use std::cell::RefCell;
let counter = Rc::new(RefCell::new(0));
let c = Rc::clone(&counter);
std::thread::spawn(move || {
*c.borrow_mut() += 1;
});
error[E0277]: `Rc<RefCell<i32>>` cannot be sent between threads safely
the trait `Send` is not implemented for `Rc<RefCell<i32>>`
Rc uses a non-atomic reference count. If two threads incremented it
simultaneously, the count could corrupt. RefCell uses a non-atomic
borrow flag. Same problem. Neither is safe to share across threads.
The parallel to Part I
The mapping is direct:
Rc->Arc(Atomic Reference Counted)RefCell->Mutex(mutual exclusion lock)Rc<RefCell<T>>->Arc<Mutex<T>>
Arc uses atomic operations for the reference count, making it safe
to clone across threads. Mutex uses OS-level locking to ensure only
one thread accesses the data at a time.
The pattern: Arc<Mutex<T>>
use std::sync::{Arc, Mutex};
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
handles.push(std::thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
}));
}
for h in handles {
h.join().unwrap();
}
println!("{}", *counter.lock().unwrap()); // 10
Arc::clone gives each thread its own handle to the shared data.
.lock() acquires the mutex, returning a guard that dereferences to
&mut T. When the guard is dropped, the lock is released. Only one
thread holds the lock at a time.
Mutex poisoning
.lock() returns Result<MutexGuard, PoisonError>.
If a thread panics while holding the lock, the mutex becomes "poisoned" -
the data inside might be in an inconsistent state.
In most programs, .lock().unwrap() is correct. If another thread
panicked, your data may be corrupt, and propagating the panic is reasonable. If
you want to recover from a poisoned mutex (because you can validate or reset the
data), use .lock().unwrap_or_else(|e| e.into_inner()).
Arc<RwLock<T>>
RwLock (read-write lock) allows multiple simultaneous readers or one
exclusive writer:
use std::sync::{Arc, RwLock};
let data = Arc::new(RwLock::new(vec![1, 2, 3]));
// Multiple readers — concurrent
let r1 = data.read().unwrap();
let r2 = data.read().unwrap();
println!("{} {}", r1.len(), r2.len()); // both held simultaneously
drop(r1);
drop(r2);
// One writer — exclusive
let mut w = data.write().unwrap();
w.push(4); // no readers or other writers can access while this is held
This mirrors the borrow checker's rules (many &T or one
&mut T), but enforced at runtime with locks.
Mutex vs RwLock
RwLock sounds strictly better - it allows concurrent reads. But
it has higher per-operation overhead: tracking the reader count requires additional
atomic operations. For a lightly contended lock or a lock where reads and writes
are roughly balanced, Mutex is faster.
Default to Mutex. Reach for RwLock when:
- Reads vastly outnumber writes (100:1 or more)
- Read operations are slow enough that concurrent reads matter
- You've measured contention and confirmed that readers are blocking each other
For most applications, Mutex is the right choice.
When to use it
- Shared counters, caches, or configuration accessed from multiple threads
- Worker pools with shared state
- Any cross-thread shared mutable state
When not to: if you can avoid sharing state entirely (message passing -
#20), do that. Shared mutable state
is harder to reason about than message passing, even with Rust's safety guarantees.
Arc<Mutex<T>> is safe, but it's still a lock, and locks can
contend and deadlock if multiple are held simultaneously.
See it in practice: Building a Chat Server #5: Going Multi-threaded uses this pattern for thread-safe shared server state.
Telex