Rust - Interior Mutability
Overview
Estimated time: 55–75 minutes
Master interior mutability in Rust - the ability to mutate data even when you have only immutable references. Learn Cell, RefCell, Mutex, RwLock, and when to use each pattern for safe concurrent and single-threaded mutation.
Learning Objectives
- Understand the concept of interior mutability
- Use Cell<T> for Copy types
- Use RefCell<T> for runtime borrow checking
- Apply Mutex<T> and RwLock<T> for thread-safe mutation
Prerequisites
What is Interior Mutability?
Interior mutability is a design pattern that allows you to mutate data even when you have immutable references to that data. It uses unsafe
code internally but provides a safe API:
use std::cell::RefCell;
fn main() {
// The RefCell itself is immutable, but its contents can be mutated
let data = RefCell::new(5);
println!("Initial value: {}", data.borrow());
// Mutate through an immutable reference
*data.borrow_mut() = 10;
println!("Modified value: {}", data.borrow());
}
Expected output:
Initial value: 5
Modified value: 10
Cell<T> - For Copy Types
Cell<T>
provides interior mutability for Copy
types through get()
and set()
:
use std::cell::Cell;
struct Counter {
value: Cell,
}
impl Counter {
fn new() -> Self {
Counter {
value: Cell::new(0),
}
}
fn increment(&self) {
let current = self.value.get();
self.value.set(current + 1);
}
fn get(&self) -> i32 {
self.value.get()
}
}
fn main() {
let counter = Counter::new();
println!("Initial: {}", counter.get());
counter.increment();
counter.increment();
println!("After increments: {}", counter.get());
// Cell allows replacement of the entire value
counter.value.set(100);
println!("After set: {}", counter.get());
}
Expected output:
Initial: 0
After increments: 2
After set: 100
Cell<T> with Custom Types
Cell works with any Copy type, including tuples and simple structs:
use std::cell::Cell;
#[derive(Copy, Clone, Debug)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let position = Cell::new(Point { x: 0, y: 0 });
println!("Initial position: {:?}", position.get());
// Move the point
position.set(Point { x: 10, y: 20 });
println!("New position: {:?}", position.get());
// Update using current value
let current = position.get();
position.set(Point {
x: current.x + 5,
y: current.y + 5
});
println!("Updated position: {:?}", position.get());
}
Expected output:
Initial position: Point { x: 0, y: 0 }
New position: Point { x: 10, y: 20 }
Updated position: Point { x: 15, y: 25 }
RefCell<T> - Runtime Borrow Checking
RefCell<T>
provides interior mutability for any type with runtime borrow checking:
use std::cell::RefCell;
struct Library {
books: RefCell>,
}
impl Library {
fn new() -> Self {
Library {
books: RefCell::new(Vec::new()),
}
}
fn add_book(&self, title: &str) {
self.books.borrow_mut().push(title.to_string());
}
fn list_books(&self) -> Vec {
self.books.borrow().clone()
}
fn book_count(&self) -> usize {
self.books.borrow().len()
}
fn find_book(&self, title: &str) -> bool {
self.books.borrow().contains(&title.to_string())
}
}
fn main() {
let library = Library::new();
println!("Initial book count: {}", library.book_count());
library.add_book("The Rust Programming Language");
library.add_book("Programming Rust");
library.add_book("Rust in Action");
println!("Books in library: {:?}", library.list_books());
println!("Total books: {}", library.book_count());
if library.find_book("Programming Rust") {
println!("Found 'Programming Rust'!");
}
}
Expected output:
Initial book count: 0
Books in library: ["The Rust Programming Language", "Programming Rust", "Rust in Action"]
Total books: 3
Found 'Programming Rust'!
RefCell Borrow Rules
RefCell enforces Rust's borrowing rules at runtime:
use std::cell::RefCell;
fn main() {
let data = RefCell::new(vec![1, 2, 3]);
// Multiple immutable borrows are OK
{
let borrow1 = data.borrow();
let borrow2 = data.borrow();
println!("Multiple immutable borrows: {:?}, {:?}", *borrow1, *borrow2);
} // borrows are dropped here
// One mutable borrow is OK
{
let mut borrow = data.borrow_mut();
borrow.push(4);
println!("After mutable borrow: {:?}", *borrow);
} // mutable borrow is dropped here
// This would panic - can't have immutable and mutable borrows simultaneously
/*
let _immutable = data.borrow();
let _mutable = data.borrow_mut(); // Panic!
*/
println!("Final data: {:?}", data.borrow());
}
Expected output:
Multiple immutable borrows: [1, 2, 3], [1, 2, 3]
After mutable borrow: [1, 2, 3, 4]
Final data: [1, 2, 3, 4]
try_borrow and Error Handling
Use try_borrow
to avoid panic on borrow conflicts:
use std::cell::RefCell;
fn safe_access(data: &RefCell>) -> Result {
match data.try_borrow() {
Ok(borrowed) => {
if borrowed.is_empty() {
Err("Vector is empty".to_string())
} else {
Ok(borrowed[0])
}
}
Err(_) => Err("Could not borrow data".to_string()),
}
}
fn main() {
let data = RefCell::new(vec![1, 2, 3]);
// Normal access
match safe_access(&data) {
Ok(value) => println!("First element: {}", value),
Err(e) => println!("Error: {}", e),
}
// Create a borrow conflict scenario
let _long_borrow = data.borrow_mut(); // Hold a mutable borrow
match safe_access(&data) {
Ok(value) => println!("First element: {}", value),
Err(e) => println!("Error: {}", e),
}
// Drop the long borrow
drop(_long_borrow);
match safe_access(&data) {
Ok(value) => println!("First element after drop: {}", value),
Err(e) => println!("Error: {}", e),
}
}
Expected output:
First element: 1
Error: Could not borrow data
First element after drop: 1
Rc<RefCell<T>> Pattern
Combine Rc
and RefCell
for shared mutable data:
use std::cell::RefCell;
use std::rc::Rc;
struct Node {
value: i32,
children: RefCell>>,
}
impl Node {
fn new(value: i32) -> Rc {
Rc::new(Node {
value,
children: RefCell::new(Vec::new()),
})
}
fn add_child(&self, child: Rc) {
self.children.borrow_mut().push(child);
}
fn print_tree(&self, indent: usize) {
println!("{:indent$}Node: {}", "", self.value, indent = indent);
for child in self.children.borrow().iter() {
child.print_tree(indent + 2);
}
}
}
fn main() {
let root = Node::new(1);
let child1 = Node::new(2);
let child2 = Node::new(3);
let grandchild = Node::new(4);
// Build the tree
child1.add_child(grandchild);
root.add_child(child1);
root.add_child(child2);
println!("Tree structure:");
root.print_tree(0);
}
Expected output:
Tree structure:
Node: 1
Node: 2
Node: 4
Node: 3
Thread-Safe Interior Mutability
Mutex<T>
Mutex<T>
provides thread-safe interior mutability:
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
// Spawn 10 threads that increment the counter
for i in 0..10 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
for _ in 0..10 {
let mut num = counter.lock().unwrap();
*num += 1;
// Simulate some work
thread::sleep(Duration::from_millis(1));
}
println!("Thread {} completed", i);
});
handles.push(handle);
}
// Wait for all threads to complete
for handle in handles {
handle.join().unwrap();
}
println!("Final counter value: {}", *counter.lock().unwrap());
}
Expected output:
Thread 0 completed
Thread 1 completed
Thread 2 completed
Thread 3 completed
Thread 4 completed
Thread 5 completed
Thread 6 completed
Thread 7 completed
Thread 8 completed
Thread 9 completed
Final counter value: 100
RwLock<T>
RwLock<T>
allows multiple readers or one writer:
use std::sync::{Arc, RwLock};
use std::thread;
use std::time::Duration;
fn main() {
let data = Arc::new(RwLock::new(vec![1, 2, 3, 4, 5]));
let mut handles = vec![];
// Spawn reader threads
for i in 0..3 {
let data = Arc::clone(&data);
let handle = thread::spawn(move || {
let reader = data.read().unwrap();
println!("Reader {}: {:?}", i, *reader);
thread::sleep(Duration::from_millis(100));
});
handles.push(handle);
}
// Spawn a writer thread
let data_writer = Arc::clone(&data);
let writer_handle = thread::spawn(move || {
thread::sleep(Duration::from_millis(50)); // Let readers start first
let mut writer = data_writer.write().unwrap();
writer.push(6);
println!("Writer: Added element 6");
thread::sleep(Duration::from_millis(100));
});
handles.push(writer_handle);
// Wait for all threads
for handle in handles {
handle.join().unwrap();
}
println!("Final data: {:?}", *data.read().unwrap());
}
Expected output:
Reader 0: [1, 2, 3, 4, 5]
Reader 1: [1, 2, 3, 4, 5]
Reader 2: [1, 2, 3, 4, 5]
Writer: Added element 6
Final data: [1, 2, 3, 4, 5, 6]
Choosing the Right Type
Different interior mutability types for different use cases:
use std::cell::{Cell, RefCell};
use std::sync::{Mutex, RwLock};
use std::rc::Rc;
use std::sync::Arc;
fn main() {
println!("Interior Mutability Type Comparison:");
println!();
// Cell - Copy types, single-threaded
let cell_counter = Cell::new(0);
cell_counter.set(cell_counter.get() + 1);
println!("Cell: {} (Copy types, no borrowing)", cell_counter.get());
// RefCell - Any type, single-threaded, runtime borrow checking
let refcell_data = RefCell::new(vec![1, 2, 3]);
refcell_data.borrow_mut().push(4);
println!("RefCell>: {:?} (Any type, runtime checking)",
refcell_data.borrow());
// Rc> - Shared ownership, single-threaded
let shared_data = Rc::new(RefCell::new(String::from("Hello")));
shared_data.borrow_mut().push_str(", World!");
println!("Rc>: {} (Shared, single-threaded)",
shared_data.borrow());
// Arc> - Shared ownership, multi-threaded, exclusive access
let threadsafe_counter = Arc::new(Mutex::new(0));
*threadsafe_counter.lock().unwrap() += 1;
println!("Arc>: {} (Thread-safe, exclusive)",
threadsafe_counter.lock().unwrap());
// Arc> - Shared ownership, multi-threaded, reader-writer lock
let threadsafe_data = Arc::new(RwLock::new(vec![1, 2, 3]));
threadsafe_data.write().unwrap().push(4);
println!("Arc>>: {:?} (Thread-safe, reader-writer)",
threadsafe_data.read().unwrap());
}
Expected output:
Interior Mutability Type Comparison:
Cell: 1 (Copy types, no borrowing)
RefCell>: [1, 2, 3, 4] (Any type, runtime checking)
Rc>: Hello, World! (Shared, single-threaded)
Arc>: 1 (Thread-safe, exclusive)
Arc>>: [1, 2, 3, 4] (Thread-safe, reader-writer)
Common Pitfalls
1. RefCell Panic on Borrow Conflicts
use std::cell::RefCell;
fn main() {
let data = RefCell::new(vec![1, 2, 3]);
// This will panic - don't do this!
/*
let _borrow1 = data.borrow();
let _borrow2 = data.borrow_mut(); // Panic: already borrowed
*/
// Safe approach - use scopes to control borrow lifetimes
{
let borrow = data.borrow();
println!("Reading: {:?}", *borrow);
} // borrow dropped here
{
let mut borrow = data.borrow_mut();
borrow.push(4);
println!("After mutation: {:?}", *borrow);
} // mutable borrow dropped here
}
2. Mutex Poisoning
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let data = Arc::new(Mutex::new(0));
let data_clone = Arc::clone(&data);
// Simulate a thread that panics while holding the lock
let handle = thread::spawn(move || {
let mut guard = data_clone.lock().unwrap();
*guard = 42;
panic!("Oops!"); // This poisons the mutex
});
// Wait for the thread to panic
let _ = handle.join();
// The mutex is now poisoned
match data.lock() {
Ok(guard) => println!("Value: {}", *guard),
Err(poisoned) => {
println!("Mutex was poisoned, recovering...");
let guard = poisoned.into_inner();
println!("Recovered value: {}", *guard);
}
}
}
Checks for Understanding
Question 1: When should you use Cell<T> vs RefCell<T>?
Answer: Use Cell<T> for Copy types when you need to replace the entire value. Use RefCell<T> for any type when you need to borrow parts of the data or when the type doesn't implement Copy.
Question 2: What happens if you violate borrowing rules with RefCell?
Answer: RefCell panics at runtime if you violate borrowing rules (e.g., trying to create a mutable borrow when immutable borrows exist). Use try_borrow methods to handle this gracefully.
Question 3: When should you choose RwLock over Mutex?
Answer: Use RwLock when you have many readers and few writers, as it allows concurrent reads. Use Mutex for simpler cases or when you have frequent writes, as it has less overhead.