Arc<T> and Mutex<T>
Safe concurrency in Rust rests on two guarantees: the ownership system prevents
data races at compile time, and the standard library's Arc and Mutex types
provide the controlled exceptions when multiple threads genuinely need to share
data.
This page covers how Arc<T> shares ownership across threads and how Mutex<T>
serialises access to mutable state — and the canonical pattern that combines both.
Arc<T>: Atomically Reference Counted
Arc<T> stands for Atomically Reference Counted. It is the thread-safe
counterpart to Rc<T>: both let multiple owners share the same heap-allocated
value, but Arc uses atomic CPU instructions to update the reference count,
making it safe to use from several threads simultaneously.
Rc<T> | Arc<T> | |
|---|---|---|
Thread-safe | No | Yes |
Reference counting | Plain integer | Atomic integer |
Performance | Faster | Slightly slower |
Use when | Single-threaded sharing | Multi-threaded sharing |
The overhead of atomic operations is usually negligible in practice, but it is
real — prefer Rc in single-threaded code and reach for Arc only when you
need to cross a thread boundary.
Creating and cloning an Arc:
use std::sync::Arc;
fn main() {
// Create a new Arc — value is heap-allocated, reference count = 1
let shared = Arc::new(vec![1, 2, 3]);
// Arc::clone increments the reference count atomically
let also_shared = Arc::clone(&shared);
println!("original: {:?}", shared);
println!("clone: {:?}", also_shared);
println!("strong count: {}", Arc::strong_count(&shared)); // 2
} // both handles drop here; count falls to 0 and the Vec is freedoriginal: [1, 2, 3] clone: [1, 2, 3] strong count: 2
Arc::clone(&arc) rather than calling.clone() on the value directly. Both compile to the same thing, but the explicit form makes it clear you are incrementing a reference count — not deep-cloning the inner data.Sharing an Arc Across Threads
Passing an Arc to a spawned thread requires moving a clone into the thread's
move closure. Each thread gets its own handle, and the underlying data is freed
only when the last handle is dropped.
use std::sync::Arc;
use std::thread;
fn main() {
let data = Arc::new(vec![10, 20, 30]);
let mut handles = vec![];
for i in 0..3 {
let data_clone = Arc::clone(&data);
let handle = thread::spawn(move || {
let sum: i32 = data_clone.iter().sum();
println!("Thread {}: sum = {}", i, sum);
});
handles.push(handle);
}
for h in handles {
h.join().unwrap();
}
}Thread 0: sum = 60 Thread 1: sum = 60 Thread 2: sum = 60
Arc<T> by itself only gives immutable shared access. If multiple threads need to mutate the shared value, pair it withMutex<T> — that is exactly what the next section covers.Mutex<T>: Mutual Exclusion
Mutex<T> wraps a value and enforces that only one thread can access it at a
time. Any thread that wants the data must first acquire the lock; all other
threads block until the current holder releases it.
In Rust, the data is stored inside the Mutex. You cannot access the value
without going through lock(), so a data race is structurally impossible.
use std::sync::Mutex;
fn main() {
let m = Mutex::new(5);
{
// lock() blocks until we acquire the lock.
// Returns MutexGuard<i32> — a smart pointer to the inner value.
let mut val = m.lock().unwrap();
*val += 1;
println!("val inside lock: {}", *val);
// MutexGuard is dropped here — lock is automatically released (RAII)
}
println!("val after lock released: {}", *m.lock().unwrap());
}val inside lock: 6 val after lock released: 6
MutexGuard implements Deref andDerefMut, so you can use it exactly like a mutable reference. When the guard goes out of scope, the lock is released automatically — this is Rust's RAII (Resource Acquisition Is Initialisation) pattern.The Canonical Pattern: Arc<Mutex<T>>
Combining Arc and Mutex gives you shared, mutable state that is safe across
threads. The Arc keeps the Mutex alive across all threads, and the Mutex
serialises write access.
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..5 {
let counter_clone = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter_clone.lock().unwrap();
*num += 1;
// num (MutexGuard) dropped here — lock released
});
handles.push(handle);
}
for h in handles {
h.join().unwrap();
}
println!("Final counter: {}", *counter.lock().unwrap());
}Final counter: 5
Every increment is protected: only one thread holds the MutexGuard at a time,
so the read-modify-write is atomic from the perspective of other threads. No data
race, no undefined behaviour.
Deadlocks
A deadlock occurs when two or more threads each hold a lock that another needs, and both wait forever for the other to release it. Rust's type system prevents data races but cannot prevent deadlocks — that is a logic error you must avoid.
std::sync::Mutex is not re-entrant: calling lock()while you already hold the lock will block the thread forever.use std::sync::{Arc, Mutex};
fn main() {
let lock = Arc::new(Mutex::new(0));
let _guard1 = lock.lock().unwrap(); // lock acquired
// The line below will hang forever — this thread already holds the lock
// let _guard2 = lock.lock().unwrap(); // DEADLOCK
}
// Safe approach — let the guard drop before re-locking
fn update_twice(m: &Mutex<i32>) {
{
let mut v = m.lock().unwrap();
*v += 1;
} // guard dropped, lock released
{
let mut v = m.lock().unwrap();
*v += 1;
} // guard dropped, lock released
}Always acquire multiple locks in the same order across all threads
Keep the scope of a lock guard as small as possible — drop it early
Never call a function that may acquire the same lock while you're holding a guard
Use
try_lock()during development to surface potential deadlock sites
Poisoned Mutexes
If a thread panics while holding a Mutex lock, the mutex is marked as
poisoned. Subsequent calls to lock() return Err(PoisonError) to prevent
other threads from silently operating on potentially inconsistent data.
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let data = Arc::new(Mutex::new(vec![1, 2, 3]));
let data_clone = Arc::clone(&data);
// This thread panics while holding the lock — mutex becomes poisoned
let _ = thread::spawn(move || {
let mut v = data_clone.lock().unwrap();
v.push(4);
panic!("something went wrong!"); // lock poisoned
})
.join(); // returns Err because the thread panicked
match data.lock() {
Ok(v) => println!("got value: {:?}", *v),
Err(poisoned) => {
// We can recover the inner value if we know it is still valid
let v = poisoned.into_inner();
println!("recovered poisoned value: {:?}", *v);
}
}
}recovered poisoned value: [1, 2, 3, 4]
.unwrap() on a poisoned lock is the right choice — if any thread panicked, you likely want to propagate the failure. Use the Err branch only when you know the partially modified state is still valid and safe to use.RwLock<T>: Multiple Readers or One Writer
RwLock<T> is a variant of Mutex<T> optimised for read-heavy workloads. It
allows either many concurrent readers or one exclusive writer — never both
simultaneously. Use it when reads far outnumber writes and you want higher
throughput.
use std::sync::{Arc, RwLock};
use std::thread;
fn main() {
let config = Arc::new(RwLock::new(String::from("initial")));
let mut handles = vec![];
// Four reader threads — they can all run concurrently
for i in 0..4 {
let cfg = Arc::clone(&config);
handles.push(thread::spawn(move || {
let val = cfg.read().unwrap(); // shared read lock
println!("Reader {}: {}", i, *val);
}));
}
// One writer — waits until all readers have released their locks
{
let cfg = Arc::clone(&config);
handles.push(thread::spawn(move || {
let mut val = cfg.write().unwrap(); // exclusive write lock
*val = String::from("updated");
println!("Writer: set to '{}'", *val);
}));
}
for h in handles {
h.join().unwrap();
}
}Mutex<T> | RwLock<T> | |
|---|---|---|
Concurrent reads | No — one thread at a time | Yes — any number of readers |
Exclusive write | Yes | Yes |
Best for | Write-heavy or balanced | Read-heavy workloads |
Deadlock risk | Lower | Slightly higher (writer starvation possible) |
Arc+Mutex vs Channels: When to Use Which
Rust gives you two primary tools for inter-thread communication:
- Channels (
std::sync::mpsc) — send messages, transfer ownership of data - Arc+Mutex — share state in-place, serialise access with a lock
Neither is universally better; they model different problems.
Scenario | Prefer |
|---|---|
One thread produces data, another consumes it | Channel |
A value is read and written by many threads | Arc+Mutex |
You want to avoid shared mutable state entirely | Channel |
Low-latency reads of cached, rarely-written data | Arc+RwLock |
Fan-out work queue | Channel with multiple senders |
Accumulating a result across threads | Arc+Mutex |