We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Atomics, Send/Sync and the Memory Model step 11 of 12
Rc<RefCell<T>> vs Arc<Mutex<T>>, and the arena that beats both
Compute the depth of every node in a rooted tree — twice, two different ways, and check that they agree.
pub fn tree_depths(edges: Vec<(usize, usize)>, threads: usize) -> Vec<usize>
edges are (parent, child) pairs; node 0 is the root and has depth 0;
every node is reachable. Return depths indexed by node. No edges means a
single node, so [0]. Treat threads == 0 as 1.
You must write both implementations:
-
An index arena. No smart pointers of any kind — a
Vec<Vec<usize>>of children and a BFS. Single-threaded. -
Shared, individually-locked nodes, walked level by level across
threadsworkers.
Then assert_eq! the two before returning. The assertion is part of the
required code: it is what turns “I read that the arena is better” into “I
built both and they agree”.
Why this problem exists
Learners meet Rc<RefCell<T>> in the Book’s graph chapter, then meet strong
opinions against it online with no framework for adjudicating. A course that
takes a defensible position and shows the alternative side by side is worth
more than either dogma.
The starter’s error
It uses Rc<RefCell<Node>>, exactly as taught, and the moment threads appear:
error[E0277]: `Rc<RefCell<Node>>` cannot be shared between threads safely
= help: the trait `Sync` is not implemented for `Rc<RefCell<Node>>`
Two independent reasons, and it is worth naming both because they are different failures:
-
Rchas a non-atomic refcount. Two threads cloning at once race on it: too low and the value is freed while alive, too high and it leaks. -
RefCellhas a non-atomic borrow flag. Two threads callingborrow_mut()at once could both see “not borrowed” and both hand out a&mut. Aliased mutable references are instant undefined behaviour.
The mechanical fix is Arc<Mutex<Node>>, or here — since the nodes live in a
Vec that outlives the scope — just Mutex<Node> shared by reference, which
is cheaper.
Notice that this is not the compiler being awkward. Every one of those types is doing exactly what it advertises, and the type system is reporting a real difference in what they guarantee.
When each shape is right
Rc<RefCell<T>> is a perfectly good tool. Single-threaded graph-shaped
data, a GUI widget tree, an interpreter’s environment chain, anything where
the ownership genuinely is a DAG and the program has one thread. It is
cheaper than the threaded version: a non-atomic increment beats an atomic
one, and a borrow-flag check beats a lock.
Its real costs are not the ones people usually cite:
-
borrow_mut()panics at runtime if the value is already borrowed. You have moved a compile-time check to a runtime one, and the failure appears under a code path you did not test. -
Cycles leak.
Rcis reference counting with no cycle collector, so a parent holding children who hold parents never frees.Weakfor back-edges is the fix, and remembering it is on you. -
It is infectious: once the data is behind
Rc<RefCell<_>>, every function that touches it inherits the ceremony.
Arc<Mutex<T>> buys thread-safety and pays with an atomic refcount, a
lock per access, poisoning, and a lock-ordering obligation the moment you need
two nodes at once.
And the arena beats both
Which is why you wrote it first.
let mut children: Vec<Vec<usize>> = vec![Vec::new(); n];
Nodes are usize indices. Look at what simply stops being a problem:
- no reference counting — the arena owns everything, and it all drops at once;
-
no cycles to leak — an index is not an owning edge, so a cyclic graph is
just a normal
Vec; -
indices are
Copy, so passing a node around costs nothing and borrows nobody; -
cache locality — one contiguous allocation instead of
nscattered ones, which for a traversal is often the whole performance story; - and it parallelises trivially, because disjoint index ranges are disjoint, as the previous problems in this track kept demonstrating.
The cost is real and worth stating: an index carries no type safety. arena[i]
where i came from a different arena is a silent bug — which is what
generational indices exist to fix.
::: question Your tree needs parent pointers as well as child pointers. What happens with each of the three designs?
Rc<RefCell<Node>> — a straight Rc parent pointer creates a cycle and
leaks the entire tree. The fix is Weak<RefCell<Node>> for the parent
edge, plus an upgrade() returning Option at every use. It works, and every
traversal now carries a None case that cannot actually happen.
Arc<Mutex<Node>> — the same leak, the same Weak fix, plus a new
hazard: walking down through a child while another thread walks up through a
parent is two locks acquired in opposite orders. That is the deadlock from
earlier in this course, arriving through a data-structure change nobody
thought of as a concurrency decision.
The arena — add a parent: usize field. That is the whole change. There
is no cycle, because an index is not an owning edge; there is nothing to
upgrade; there is no second lock.
This is the strongest argument for the arena, and it is not about performance: the pointer-based designs make cyclic references a memory-management problem, and the arena makes them a non-event. :::
The lints in this neighbourhood
-
clippy::rc_mutexflagsRc<Mutex<T>>, which is almost always a confusion — single-threaded ownership around a thread-safe lock. It isrestriction, so off by default. -
clippy::arc_with_non_send_syncflagsArc::new(x)wherexis neitherSendnorSync: such anArccan never cross a thread boundary, so you paid for atomic refcounting for nothing. On by default. -
clippy::redundant_allocationcatchesArc<Box<T>>and friends. -
clippy::type_complexitywill eventually object to nested wrapper types. The fix is a type alias, not an#[allow]— and if the alias is hard to name, that is a signal about the design.
Loading visualization…
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.