Interior Mutability
Choosing the Appropriate Primitive
Rust provides a specialized suite of interior mutability primitives across std::cell and std::sync so you never pay for synchronization, atomic instructions, or runtime borrow counters that your access pattern does not require.
Decision Tree for Interior Mutability Primitives
Need to mutate state through a shared reference (`&T`)?
|
+-- 1. Is the value accessed across multiple threads (`Sync` required)?
| |
| +-- NO (Single-Threaded -> `std::cell`)
| | +-- Initialized once, then read-only `&T`?
| | | +-- Initializer fixed at creation ---------> `LazyCell<T>`
| | | +-- Initialized on demand / with args -----> `OnceCell<T>`
| | +-- Mutated repeatedly by value (`Copy`, `Default`, or swap)?
| | | +------------------------------------------> `Cell<T>`
| | +-- Need `&T` and `&mut T` references to inner value?
| | +------------------------------------------> `RefCell<T>`
| |
| +-- YES (Multi-Threaded -> `std::sync` / `std::sync::atomic`)
| +-- Initialized once, then read-only `&T`?
| | +-- Initializer fixed (`static` global) ---> `LazyLock<T>`
| | +-- Initialized on demand / with args -----> `OnceLock<T>`
| +-- Single primitive integer / boolean / pointer?
| | +------------------------------------------> `AtomicBool` / `AtomicUsize` / `AtomicU64`
| +-- Arbitrary struct / collection mutated repeatedly?
| +-- General exclusive read/write access ---> `Mutex<T>`
| +-- High read concurrency, infrequent writes -> `RwLock<T>`
Complete Reference Table
| Primitive | Module | Thread Safe (Sync) |
Returns &T Directly? |
Returns Guard (Deref)? |
Can Panic or Deadlock on Access? | Extra Size Overhead (64-bit) |
|---|---|---|---|---|---|---|
Cell<T> |
std::cell |
No | No (copies/moves T) |
No | Never | 0 bytes |
RefCell<T> |
std::cell |
No | No | Ref<T> / RefMut<T> |
Panics on conflicting borrow | 8 bytes (isize) |
OnceCell<T> |
std::cell |
No | Yes (Option<&T>) |
No | Panics only on re-entrant get_or_init |
Discriminant of Option<T> |
LazyCell<T, F> |
std::cell |
No | Yes (via Deref) |
No (LazyCell itself derefs) |
Panics only on re-entrant init | Size of F / discriminant |
AtomicU64 / AtomicBool |
std::sync::atomic |
Yes | No (loads/stores value) | No | Never (lock-free hardware ops) | 0 bytes |
OnceLock<T> |
std::sync |
Yes | Yes (Option<&T>) |
No | Deadlocks only on recursive get_or_init |
Once state (4–8 bytes) + Option<T> |
LazyLock<T, F> |
std::sync |
Yes | Yes (via Deref) |
No (LazyLock itself derefs) |
Deadlocks/panics only on recursive init | Once state + size of F |
Mutex<T> |
std::sync |
Yes (T: Send) |
No | MutexGuard<'_, T> |
Can deadlock if relocked on same thread; poisons on panic | OS mutex header (8–40 bytes) |
RwLock<T> |
std::sync |
Yes (T: Send + Sync) |
No | RwLockReadGuard / RwLockWriteGuard |
Can deadlock if relocked on same thread; poisons on write panic | OS rwlock header (8–56 bytes) |
Combining Multiple Primitives in One Service Struct
Real-world types frequently combine several interior mutability primitives—choosing the lightest primitive for each individual field's access pattern:
use std::cell::{Cell, OnceCell, RefCell};
struct HttpClient {
// 1. Write-once lazy TLS root certificate store -> `OnceCell` (returns `&str` with no guard):
user_agent: OnceCell<String>,
// 2. Simple `Copy` counter -> `Cell` (0 bytes overhead, never panics):
total_requests: Cell<u64>,
// 3. Growable collection borrowed by reference -> `RefCell`:
recent_urls: RefCell<Vec<String>>,
}
impl HttpClient {
fn new() -> Self {
Self {
user_agent: OnceCell::new(),
total_requests: Cell::new(0),
recent_urls: RefCell::new(Vec::new()),
}
}
fn agent(&self) -> &str {
self.user_agent
.get_or_init(|| String::from("rust-http-client/1.0"))
}
fn get(&self, url: &str) {
self.total_requests.set(self.total_requests.get() + 1);
self.recent_urls.borrow_mut().push(url.to_string());
println!("[GET #{}] {} (UA: {})", self.total_requests.get(), url, self.agent());
}
}
fn main() {
let client = HttpClient::new();
client.get("https://example.com/status");
client.get("https://example.com/metrics");
println!("recent_urls: {:?}", client.recent_urls.borrow());
}
[GET #1] https://example.com/status (UA: rust-http-client/1.0)
[GET #2] https://example.com/metrics (UA: rust-http-client/1.0)
recent_urls: ["https://example.com/status", "https://example.com/metrics"]