Interior Mutability
Mutex<T> and RwLock<T>
std::sync::Mutex<T> and std::sync::RwLock<T> are the multi-threaded counterparts to RefCell<T>. Whereas RefCell<T> panics immediately when two conflicting borrows occur on the same thread, Mutex<T> and RwLock<T> coordinate concurrent access across multiple threads by blocking waiting threads in the operating-system scheduler until the active guard (MutexGuard, RwLockReadGuard, or RwLockWriteGuard) is dropped.
Architectural Parallel with Single-Threaded Primitives
Single-Threaded (std::cell) |
Multi-Threaded (std::sync) |
Access Mechanism | Conflict Resolution |
|---|---|---|---|
Cell<T> (Copy primitives) |
AtomicBool, AtomicU64, AtomicUsize |
Lock-free by-value load / store / fetch-add | Hardware cache-coherency instructions |
OnceCell<T> / LazyCell<T> |
OnceLock<T> / LazyLock<T> |
Write-once initialization returning &T |
Blocks concurrent initializers once; lock-free &T thereafter |
RefCell<T> |
Mutex<T> |
Exclusive MutexGuard<'_, T> (&mut T) |
Blocks contending threads until guard drops |
RefCell<T> |
RwLock<T> |
Many RwLockReadGuard (&T) XOR one RwLockWriteGuard (&mut T) |
Readers run concurrently; writers wait for readers and block new readers |
Comparing Mutex<T> and RwLock<T> Trade-Offs
Mutex<T> (Single Queue — All Readers and Writers Serialized):
[Reader 1] -> [Reader 2] -> [Writer 1] -> [Reader 3] (One at a time)
RwLock<T> (Concurrent Read Phase + Exclusive Write Phase):
[Reader 1] \
[Reader 2] +--> (Run in parallel) ---> [Writer 1 (Exclusive)] ---> [Reader 3]
[Reader 3] /
| Criterion | Mutex<T> |
RwLock<T> |
|---|---|---|
| Read concurrency | Serialized (1 thread at a time) |
Parallel ($N$ reader threads simultaneously) |
| Lock acquisition overhead | Lower (single atomic state word) | Slightly higher (tracks reader count + writer state) |
Required trait bounds for Sync |
T: Send |
T: Send + Sync |
| Writer starvation risk | None (fair queue on modern OS futexes) | Possible on read-dominated workloads if readers continuously overlap (platform-dependent) |
| Best suited for | Fast mutations, short critical sections, or non-Sync inner types |
Read-heavy caches or routing tables where read operations are frequent or non-trivial |
Combining Mutex<T> and RwLock<T> in Concurrent State
use std::sync::{Mutex, RwLock};
use std::thread;
struct MetricsRegistry {
// Read-heavy configuration:
service_tags: RwLock<Vec<String>>,
// Write-heavy counter/histogram state:
request_latencies_ms: Mutex<Vec<u32>>,
}
impl MetricsRegistry {
fn new() -> Self {
Self {
service_tags: RwLock::new(vec![String::from("env:prod")]),
request_latencies_ms: Mutex::new(Vec::new()),
}
}
fn record_latency(&self, ms: u32) {
self.request_latencies_ms.lock().unwrap().push(ms);
}
fn format_report(&self) -> String {
let tags = self.service_tags.read().unwrap();
let count = self.request_latencies_ms.lock().unwrap().len();
format!("tags={:?} samples={}", *tags, count)
}
}
fn main() {
let registry = MetricsRegistry::new();
thread::scope(|s| {
for ms in [12, 18, 9, 25] {
s.spawn(|| registry.record_latency(ms));
}
s.spawn(|| {
registry
.service_tags
.write()
.unwrap()
.push(String::from("region:ap-south"));
});
});
println!("{}", registry.format_report());
}
tags=["env:prod", "region:ap-south"] samples=4
Bypassing Lock Overhead When Exclusive &mut Is Held
Both Mutex::get_mut(&mut self) and RwLock::get_mut(&mut self) return LockResult<&mut T> without acquiring the underlying OS lock. Because the caller proves at compile time via &mut that no other thread holds a reference to the Mutex or RwLock, batch updates during single-threaded setup or teardown pay zero synchronization overhead.