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.