Chapter 12 — Smart Pointers and Shared State
Goal: Choose Box, Rc, Arc, RefCell, and Mutex by answering two questions: who shares ownership, and where is mutation checked?
Consider this attempt to share a counter with another thread:
use std::cell::RefCell;
use std::rc::Rc;
fn main() {
let count = Rc::new(RefCell::new(0));
let other = Rc::clone(&count);
std::thread::spawn(move || *other.borrow_mut() += 1);
}
Prediction
Why does it fail?
A. RefCell can never contain an integer.
B. spawn accepts only functions, not closures.
C. Rc<RefCell<i32>> is not safe to send between threads.
D. The integer needs to be boxed first.
RevealC
Condensed compiler evidence:
error[E0277]: `Rc<RefCell<i32>>` cannot be sent between threads safely
--> ... std::thread::spawn(move || ...)
= help: the trait `Send` is not implemented for `Rc<RefCell<i32>>`
The pointer names are not a ladder from primitive to advanced. Each buys a specific capability at a specific enforcement point.
| Type | Ownership | Mutation rule | Thread use |
|---|---|---|---|
Box<T> |
one owner | normal & / &mut |
if T permits |
Rc<T> |
shared, reference counted | shared access only by itself | single-thread only |
Arc<T> |
shared, atomic reference counted | shared access only by itself | cross-thread if T permits |
RefCell<T> |
does not add ownership | borrow rule checked at runtime | single-thread only |
Mutex<T> |
does not add ownership | one lock guard at runtime | cross-thread synchronization |
Box<T> puts a value on the heap while retaining one owner. Its important use is often not “large data,” but giving a recursive or trait-object value a known size:
enum List {
Node(i32, Box<List>),
End,
}
Without Box, List would contain another full List forever and have no finite compile-time size.
Rc<T> allows multiple owners in one thread. Cloning an Rc increments a count; it does not clone T. When the last strong owner drops, T drops. Reference-count cycles leak, so parent links in graph-like structures often use Weak<T>.
Arc<T> provides the same ownership shape with atomic count updates. Atomics make the count safe across threads, but do not make T mutable or automatically thread-safe. Arc<RefCell<T>> still fails because RefCell<T> is not Sync.
RefCell<T> moves the shared-versus-exclusive borrow check to runtime. borrow() and borrow_mut() return guards. Conflicting guards panic:
use std::cell::RefCell;
let value = RefCell::new(5);
let read = value.borrow();
let _write = value.borrow_mut(); // panics: already borrowed
println!("{read}");
This is interior mutability: an outer shared reference can initiate mutation because the inner type enforces the rule. It is useful when an API must expose &self but internally updates caches, mocks, or graph nodes. It is not a way to disable borrowing rules.
Mutex<T> also provides interior mutability, but blocks competing threads rather than panicking on ordinary contention. Locking returns a guard that acts like &mut T; dropping the guard unlocks. If a thread panics while holding the lock, later lock calls report poisoning through Result.
The simplest fix for the original program is Arc<Mutex<T>>:
use std::sync::{Arc, Mutex};
fn main() {
let count = Arc::new(Mutex::new(0));
let other = Arc::clone(&count);
let handle = std::thread::spawn(move || {
*other.lock().unwrap() += 1;
});
handle.join().unwrap();
println!("{}", *count.lock().unwrap()); // 1
}
Keep guards short; holding a lock while doing slow work increases contention and can create deadlocks. The main tradeoff alternative is message passing with std::sync::mpsc: one thread owns the state and others send updates. That avoids shared mutation but introduces a protocol and channel failure handling.
Working model: pointer type chooses ownership; cell or lock type chooses how mutation is coordinated.
Precise model: Send permits ownership transfer between threads, while Sync permits shared references across threads. These are unsafe marker traits implemented by types whose internals uphold the required guarantees.
Mini challenge
Choose the smallest fitting type for a syntax tree where many nodes share immutable source text, all work stays on one thread, and the text never changes.
A. Box<String>
B. Rc<String>
C. Arc<Mutex<String>>
D. RefCell<String>
Answer: B. The requirement is shared ownership, single-threaded, immutable data. Rc<String> states exactly that. Arc<String> is a valid tradeoff if the tree later crosses threads, but its atomic counting pays for a capability not currently required.