Mutex & RwLock
When threads must share mutable state, Mutex and RwLock serialize access so only one writer or many readers touch data at a time. Pair them with Arc for multi-thread sharing.
Recipe
Quick-reference recipe card - copy-paste ready.
use std::sync::{Arc, Mutex};
fn main() {
let data = Arc::new(Mutex::new(Vec::new()));
let mut guard = data.lock().unwrap();
guard.push(1);
}When to reach for this: Shared counters, caches, or in-memory state updated by multiple threads when message passing is awkward.
Working Example
use std::sync::{Arc, RwLock};
use std::thread;
#[derive(Default)]
struct Cache {
entries: RwLock<std::collections::HashMap<String, String>>,
}
fn main() {
let cache = Arc::new(Cache::default());
let mut handles = vec![];
for i in 0..8 {
let cache = Arc::clone(&cache);
handles.push(thread::spawn(move || {
let key = format!("key-{i}");
if i % 3 == 0 {
let mut map = cache.entries.write().unwrap();
map.insert(key, format!("value-{i}"));
} else {
let map = cache.entries.read().unwrap();
let _ = map.get(&key);
}
}));
}
for h in handles {
h.join().unwrap();
}
}What this demonstrates:
RwLockallows concurrent readers.writeexcludes all other access.Arcshares the lock across threads.
Deep Dive
How It Works
- Mutex: Exclusive access. One guard at a time. Simple and predictable.
- RwLock: Many readers OR one writer. Better when reads dominate.
- Poisoning: If a thread panics while holding a lock, the mutex is "poisoned".
lock()returnsErr- recover withinto_inner()or propagate failure. - Parking: Contended locks put threads to sleep instead of busy-spinning (OS-dependent).
std::sync vs parking_lot
| Crate | Pros | Cons |
|---|---|---|
std::sync::Mutex | No extra dependency | Slightly slower, poisoning model |
parking_lot::Mutex | Faster, no poisoning | External dependency |
Rust Notes
// Keep critical sections short
{
let mut guard = mutex.lock().unwrap();
guard.push(item);
} // guard dropped here - lock released before slow I/O
// Never hold a lock across .await in async codeMutexGuardderefs to inner data viaDerefMut.- Prefer
parking_lotin hot paths when profiling shows lock overhead. - Consider
DashMapfor sharded concurrent hash maps.
Gotchas
- Lock ordering deadlocks - Thread A locks
m1thenm2; B does reverse. Fix: Always acquire locks in a fixed global order. - Long critical sections - Holding locks during I/O blocks all waiters. Fix: Clone data out, release lock, then do I/O.
- RwLock for write-heavy workloads - Writers starve readers and each other. Fix: Use
Mutexor channels. - Mutex for read-only data - Unnecessary contention. Fix: Use
Arc<T>when data is immutable after construction. - Ignoring poison errors -
unwrap()on poison may hide cascading failures. Fix: Log and reset state or shut down the service.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Channels | Producers hand off owned work | Many threads need to read the same snapshot repeatedly |
| Atomics | Simple counters and flags | Complex data structures |
DashMap / sharded locks | High-concurrency maps | You need strict serializability on one object |
Immutable Arc + replacement | Rare updates | Frequent in-place mutation |
FAQs
Mutex or RwLock?
Use RwLock when reads are much more frequent than writes. Otherwise Mutex is simpler and often faster under write load.
What is lock poisoning?
A panic while holding a lock marks it poisoned. Other threads detect possible inconsistent state via lock() returning Err.
Can I use try_lock?
Yes. try_lock returns immediately with Ok or Err instead of blocking - useful for avoiding deadlocks or skipping work.
Why parking_lot?
Smaller, faster locks without the std poisoning overhead. Common in production Rust codebases.
Mutex in async Tokio?
Use tokio::sync::Mutex for async-aware locking, or avoid locks with message passing. Never hold std::sync::MutexGuard across .await.
How do I test lock contention?
Load-test with many threads and measure p99 latency. If contention is high, shard data or switch to message passing.
Recursive locking?
std::sync::Mutex is not recursive. parking_lot::ReentrantMutex exists if you truly need re-entry (rare).
Reader starvation?
Some RwLock implementations starve writers or readers under extreme load. Check docs or use Mutex if fairness matters.
Single-threaded Mutex?
std::sync::Mutex still works on one thread and gives interior mutability with 'static sharing via Arc later.
When to avoid locks entirely?
When you can send messages, use atomics, or partition data so each thread owns its slice (Rayon, sharding).
Related
- Arc for Shared State -
Arc<Mutex<T>>pattern - Send & Sync - why locks are Sync
- Deadlocks & Race Conditions - lock ordering
- Shared State in Async - async mutex patterns
- Concurrency Best Practices - minimize lock scope
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.