When to use Cell, RefCell, and Mutex
Interior mutability is a design pattern in Rust that allows you to mutate data even when there are immutable references to that data. This seemingly breaks Rust's borrowing rules, but it does so safely by moving the borrow checking from compile-time to runtime (or using atomics for thread-safety).
The Core Principle:&T = immutable, &mut T = mutable (enforced at compile-time)&Cell or &RefCell = can mutate through immutable reference (checked at runtime)use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
/// An HTTP cache that can be shared across request handlers
/// Uses RefCell for interior mutability - cache updates don't require &mut self
struct HttpCache {
cache: RefCell<HashMap<String, CachedResponse>>,
max_size: usize,
}
#[derive(Clone)]
struct CachedResponse {
body: String,
headers: Vec<(String, String)>,
status: u16,
timestamp: u64,
}
impl HttpCache {
fn new(max_size: usize) -> Self {
Self {
cache: RefCell::new(HashMap::new()),
max_size,
}
}
/// Get from cache - note the &self, not &mut self!
fn get(&self, url: &str) -> Option<CachedResponse> {
// Borrow the cache immutably
let cache = self.cache.borrow();
cache.get(url).cloned()
}
/// Store in cache - still only needs &self
fn set(&self, url: String, response: CachedResponse) {
// Borrow the cache mutably at runtime
let mut cache = self.cache.borrow_mut();
// Evict old entries if cache is full
if cache.len() >= self.max_size {
if let Some(oldest_key) = cache.keys().next().cloned() {
cache.remove(&oldest_key);
}
}
cache.insert(url, response);
}
/// Clear expired entries - also only needs &self
fn evict_expired(&self, current_time: u64, ttl: u64) {
let mut cache = self.cache.borrow_mut();
cache.retain(|_, response| {
current_time - response.timestamp < ttl
});
}
}
// Usage in a web server context
fn handle_request(url: &str, cache: &HttpCache) -> String {
// Try cache first
if let Some(cached) = cache.get(url) {
println!("Cache HIT for {}", url);
return cached.body;
}
println!("Cache MISS for {}", url);
// Fetch from network (simulated)
let response = CachedResponse {
body: format!("Response for {}", url),
headers: vec![("Content-Type".to_string(), "text/html".to_string())],
status: 200,
timestamp: current_timestamp(),
};
// Store in cache - note we can do this with just &cache!
cache.set(url.to_string(), response.clone());
response.body
}
fn current_timestamp() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs()
}
&HttpCache, not &mut HttpCacheuse std::cell::RefCell;
use std::collections::VecDeque;
use std::rc::Rc;
/// A simple database connection pool
/// Uses RefCell to manage available connections without &mut self
struct ConnectionPool {
available: RefCell<VecDeque<Connection>>,
max_connections: usize,
created_count: RefCell<usize>,
}
struct Connection {
id: usize,
// In real code: TCP stream, authentication state, etc.
}
impl ConnectionPool {
fn new(max_connections: usize) -> Self {
Self {
available: RefCell::new(VecDeque::new()),
max_connections,
created_count: RefCell::new(0),
}
}
/// Acquire a connection from the pool
/// Note: &self, not &mut self
fn acquire(&self) -> Result<Connection, &'static str> {
// Try to get an available connection
if let Some(conn) = self.available.borrow_mut().pop_front() {
return Ok(conn);
}
// No available connections - create a new one if under limit
let mut count = self.created_count.borrow_mut();
if *count < self.max_connections {
*count += 1;
let conn = Connection { id: *count };
println!("Created new connection {}", conn.id);
Ok(conn)
} else {
Err("Connection pool exhausted")
}
}
/// Return a connection to the pool
fn release(&self, conn: Connection) {
println!("Releasing connection {}", conn.id);
self.available.borrow_mut().push_back(conn);
}
/// Get pool statistics without modifying anything
fn stats(&self) -> (usize, usize) {
(
self.available.borrow().len(),
*self.created_count.borrow(),
)
}
}
// Usage
fn perform_database_query(pool: &ConnectionPool, query: &str) -> Result<(), &'static str> {
// Acquire connection
let conn = pool.acquire()?;
// Use connection
println!("Executing query on connection {}: {}", conn.id, query);
// Return to pool when done
pool.release(conn);
Ok(())
}
use std::cell::Cell;
struct Counter {
count: Cell<u32>, // u32 is Copy
}
impl Counter {
fn new() -> Self {
Self { count: Cell::new(0) }
}
fn increment(&self) { // Note: &self not &mut self
let current = self.count.get();
self.count.set(current + 1);
}
fn get(&self) -> u32 {
self.count.get()
}
}
Use Cell when:
Copy (integers, floats, bool, etc.)use std::cell::RefCell;
struct Logger {
logs: RefCell<Vec<String>>, // Vec is NOT Copy
}
impl Logger {
fn new() -> Self {
Self { logs: RefCell::new(Vec::new()) }
}
fn log(&self, message: String) {
// Borrow mutably, modify, automatic drop releases borrow
self.logs.borrow_mut().push(message);
}
fn print_logs(&self) {
// Borrow immutably to read
for log in self.logs.borrow().iter() {
println!("{}", log);
}
}
}
Use RefCell when:
let logger = Logger::new();
let borrow1 = logger.logs.borrow();
let borrow2 = logger.logs.borrow_mut(); // PANIC! Already borrowed immutably
use std::sync::Mutex;
use std::thread;
struct SharedCounter {
count: Mutex<u32>, // Thread-safe interior mutability
}
impl SharedCounter {
fn new() -> Self {
Self { count: Mutex::new(0) }
}
fn increment(&self) {
let mut count = self.count.lock().unwrap();
*count += 1;
} // Lock automatically released when guard drops
fn get(&self) -> u32 {
*self.count.lock().unwrap()
}
}
// Can be safely shared across threads
fn use_across_threads() {
let counter = std::sync::Arc::new(SharedCounter::new());
let mut handles = vec![];
for _ in 0..10 {
let counter_clone = counter.clone();
let handle = thread::spawn(move || {
for _ in 0..100 {
counter_clone.increment();
}
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Final count: {}", counter.get()); // Should be 1000
}
Use Mutex when:
use std::cell::RefCell;
use std::collections::HashMap;
/// Secure token storage with automatic expiration
/// Uses RefCell for thread-local token management
struct TokenStore {
tokens: RefCell<HashMap<String, Token>>,
}
struct Token {
value: String,
issued_at: u64,
expires_at: u64,
permissions: Vec<String>,
}
impl TokenStore {
fn new() -> Self {
Self {
tokens: RefCell::new(HashMap::new()),
}
}
/// Issue a new token
fn issue_token(&self, user_id: &str, permissions: Vec<String>, ttl_secs: u64) -> String {
let now = current_timestamp();
let token_value = format!("token_{}_{}", user_id, now);
let token = Token {
value: token_value.clone(),
issued_at: now,
expires_at: now + ttl_secs,
permissions,
};
self.tokens.borrow_mut().insert(token_value.clone(), token);
token_value
}
/// Validate token and check permissions
fn validate(&self, token_value: &str, required_permission: &str) -> Result<(), &'static str> {
let now = current_timestamp();
let tokens = self.tokens.borrow();
let token = tokens.get(token_value).ok_or("Invalid token")?;
// Check expiration
if now > token.expires_at {
return Err("Token expired");
}
// Check permissions
if !token.permissions.contains(&required_permission.to_string()) {
return Err("Insufficient permissions");
}
Ok(())
}
/// Revoke a token
fn revoke(&self, token_value: &str) {
self.tokens.borrow_mut().remove(token_value);
}
/// Clean up expired tokens
fn cleanup_expired(&self) {
let now = current_timestamp();
self.tokens.borrow_mut().retain(|_, token| now <= token.expires_at);
}
}
// BAD: Borrow held across function calls
fn bad_usage(cache: &HttpCache) {
let cache_borrow = cache.cache.borrow(); // Borrow starts
// ... lots of code ...
some_function_that_might_borrow_cache(cache); // PANIC if it tries to borrow_mut!
// Borrow only released here
}
// GOOD: Minimize borrow scope
fn good_usage(cache: &HttpCache) {
let data = {
let cache_borrow = cache.cache.borrow();
cache_borrow.get("key").cloned()
}; // Borrow released here
some_function_that_might_borrow_cache(cache); // Safe!
}
// BAD: RefCell is NOT thread-safe!
use std::cell::RefCell;
use std::sync::Arc;
let data = Arc::new(RefCell::new(vec![1, 2, 3])); // ā RefCell is not Send!
// Won't compile - RefCell cannot be shared across threads
// GOOD: Use Mutex for thread-safety
use std::sync::Mutex;
let data = Arc::new(Mutex::new(vec![1, 2, 3])); // ā
Mutex is Send + Sync
// BAD: Using RefCell when &mut self would work
struct Config {
settings: RefCell<HashMap<String, String>>,
}
impl Config {
fn update(&self, key: String, value: String) { // Takes &self
self.settings.borrow_mut().insert(key, value);
}
}
// GOOD: Use &mut self if you control the API
struct Config {
settings: HashMap<String, String>,
}
impl Config {
fn update(&mut self, key: String, value: String) { // Takes &mut self
self.settings.insert(key, value);
}
}
Rule of Thumb: Only use interior mutability when you truly cannot get &mut self.
Copy types| Type | Thread-Safe | Runtime Cost | Panic Risk | Use Case |
|------|-------------|--------------|------------|----------|
| Cell | ā | Zero | None | Copy types, single-threaded |
| RefCell | ā | Low | Yes | Non-Copy types, single-threaded |
| Mutex | ā
| Medium-High | No (deadlock risk) | Multi-threaded |
| RwLock | ā
| Medium-High | No (deadlock risk) | Multi-threaded, read-heavy |
| Atomic* | ā
| Low | None | Primitive types, lock-free |
Create an LRU (Least Recently Used) cache using RefCell and track access order.
HashMap for O(1) lookupsVecDeque or linked list for LRU orderingImplement an observer pattern where subjects notify observers without needing &mut self.
Hints:observers: RefCell>> Create a thread-local event logger that multiple components can write to without &mut access.
Hints:thread_local! with RefCellTokio's runtime uses RefCell internally for thread-local task queues.
Serde uses interior mutability for deserializer state that needs mutation during deserialization.
View on GitHubActix uses Arc for shared application state across handlers.
Run this code in the official Rust Playground