Shared state
Share data safely across threads using Arc for reference counting and Mutex or RwLock for interior mutability — and understand Send and Sync.
- Use Arc<T> to share ownership of data across threads
- Lock a Mutex<T> to gain exclusive mutable access
- Use RwLock<T> for concurrent read access with exclusive write access
- Explain the Send and Sync marker traits and what they guarantee
- Identify deadlock patterns and how to avoid them
When threads need to read or modify the same data, ownership alone isn't enough — one value can't have multiple owners at once. Rust provides two composable tools: Arc<T> for shared ownership and Mutex<T> or RwLock<T> for safe mutation. Combined, they form the idiomatic pattern for shared mutable state.
Arc<T>: shared ownership across threads
Arc is "Atomically Reference Counted." Like Rc<T> (the single-threaded version), it lets multiple owners share a value. Unlike Rc<T>, Arc uses atomic operations for its reference count — safe to use across threads.
use std::sync::Arc;
use std::thread;
fn main() {
let data = Arc::new(vec![1, 2, 3]);
let handles: Vec<_> = (0..3).map(|_| {
let data = Arc::clone(&data); // clone the Arc, not the Vec
thread::spawn(move || {
println!("{:?}", data);
})
}).collect();
for handle in handles {
handle.join().unwrap();
}
}Arc::clone increments the reference count. When the last Arc pointing to the data is dropped, the data is freed. Each thread gets its own Arc handle — all pointing at the same Vec.
Arc alone only gives shared immutable access. If you need to mutate the shared data, wrap it in a Mutex or RwLock inside the Arc.
Mutex<T>: exclusive access
Mutex provides mutual exclusion: at most one thread can access the inner value at a time. You call lock() to acquire the lock; it returns a MutexGuard that derefs to &mut T. When the guard is dropped, the lock is released.
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0u64));
let handles: Vec<_> = (0..10).map(|_| {
let counter = Arc::clone(&counter);
thread::spawn(move || {
let mut guard = counter.lock().unwrap();
*guard += 1;
})
}).collect();
for handle in handles { handle.join().unwrap(); }
println!("Final: {}", counter.lock().unwrap()); // 10
}Arc<Mutex<T>> is the canonical pattern for shared mutable state in Rust.
Deadlock patterns
A deadlock occurs when two threads each hold a lock the other is waiting for. Rust's type system doesn't prevent deadlocks — they're a logic problem, not a type problem:
// Thread A holds lock1, waiting for lock2
// Thread B holds lock2, waiting for lock1
// — both wait foreverCommon rules to avoid deadlocks:
- Consistent lock ordering: always acquire multiple locks in the same order everywhere.
- Minimal lock scope: hold locks for the shortest time possible. Don't call user code while holding a lock.
- Prefer channels when you can: pass data instead of sharing it.
Calling lock().unwrap() will panic if the mutex is poisoned — which happens when another thread panicked while holding the lock. In production code, handle PoisonError explicitly or use lock().unwrap_or_else(|e| e.into_inner()) to recover the guard despite the poison.
RwLock<T>: concurrent reads, exclusive writes
RwLock allows multiple concurrent readers or one exclusive writer, but never both. This is more efficient than a Mutex when reads are frequent and writes are rare:
use std::sync::{Arc, RwLock};
use std::thread;
fn main() {
let config = Arc::new(RwLock::new(String::from("initial")));
// Multiple reader threads
let readers: Vec<_> = (0..3).map(|i| {
let config = Arc::clone(&config);
thread::spawn(move || {
let value = config.read().unwrap();
println!("reader {}: {}", i, *value);
})
}).collect();
// One writer thread
let config_w = Arc::clone(&config);
let writer = thread::spawn(move || {
let mut value = config_w.write().unwrap();
*value = String::from("updated");
});
for r in readers { r.join().unwrap(); }
writer.join().unwrap();
}Send and Sync marker traits
Two marker traits underpin Rust's thread safety:
Send: a type can be transferred to another thread. Most types areSend.Rc<T>is not (its reference count isn't atomic).Sync: a type can be shared across threads (i.e.,&TisSend).Mutex<T>isSync(sharing a reference to it is safe);Cell<T>is not.
These are auto-implemented by the compiler based on the types of a struct's fields. You rarely implement them manually. When the compiler says "the trait Send is not implemented," it's telling you that something in your type can't safely cross thread boundaries.
// This won't compile:
let rc = std::rc::Rc::new(42);
thread::spawn(move || println!("{}", rc));
// error: Rc<i32> cannot be sent between threads safely
// Use Arc<i32> instead.Check your understanding
Knowledge check
- 1.Why must you use Arc<T> instead of Rc<T> for shared ownership across threads?
- 2.How does Mutex<T> ensure exclusive access without requiring you to call an explicit unlock?
- 3.When is RwLock a better choice than Mutex?
Do it yourself
Build a thread-safe hit counter: spawn 20 threads, each incrementing a Arc<Mutex<u64>> 1000 times. Join all threads. The final count should be exactly 20,000. If you see a different number, you have a synchronisation bug — the type system won't let you have one here, but it's useful to verify.
Where to go next
Shared state with locks is one model for inter-thread communication. The other model — channels — passes data between threads rather than sharing it. Channels are often simpler and less error-prone. Next lesson covers std::sync::mpsc.