NovFora Dev

Thread Rust's borrow checker — when to use Arc<Mutex<T>> vs. Rc<RefCell<T>> in single-threaded contexts

Taylor Davis

Taylor Davis

2 months ago

I am building a tree traversal that needs shared ownership of nodes with interior mutability, but I want to avoid the overhead of atomic operations where possible since this is strictly single-threaded. The standard answer seems to be Rc<RefCell<T>>, but there are several edge cases I would like clarity on: 1) does RefCell's runtime borrow checking introduce measurable overhead in tight loops compared to a raw pointer approach? (I have heard conflicting reports about the cost of borrow_mut()), 2) at what scale should I consider moving toward an arena-based ECS style where ownership is flat and references are indices instead of smart pointers, and 3) how does this compare to using a typed index into a Vec<Node> — i.e., is Rc/RefCell ever genuinely preferable over manual

Joseph Adams

Joseph Adams

2 months ago

This question is essentially asking about a choice between two different mechanisms for interior mutability under different ownership semantics, and we should be precise here because each has very specific failure modes that people frequently conflate with one another.

Let me start by laying out the taxonomy clearly so there is no ambiguity in what we are comparing. Rc<RefCell<T>> gives you single-threaded shared ownership of a type T where mutation can happen at any time through borrow_mut(), which panics if there are already active mutable borrows (or any active borrows, depending on the method). The entire invariant is enforced at runtime via RefCell's internal counter. Arc<Mutex<T>> gives you thread-safe shared ownership of a type T where mutation happens by acquiring a LockGuard, and concurrency conflicts are handled through blocking rather than panicking — though Mutex in Rust will also panic if poisoned (if a previous lock holder panicked while holding the mutex).

Now for the single-threaded context specifically. The author asks when to use which. Technically, Arc<Mutex<T>> is almost never the right answer in purely single-threaded code because you are paying the atomic reference counting overhead of Arc (atomic add/sub on every clone/drop) and the lock acquisition overhead of Mutex (even if uncontended it involves a compare-and-swap), both of which are non-trivial compared to their non-atomic counterparts. But there is a semantic nuance here that people often miss: Rc<RefCell<T>> requires you to be willing to handle runtime panic on reborrow, whereas Arc<Mutex<T>> does not — it just blocks or panics via poisoning. This means if your code has complex control flow where it's possible for a borrow to span a call that itself might attempt to borrow the same RefCell again (e.g., through callbacks), Rc<RefCell<T>> will blow up and you can either refactor out the reborrow or

Join the conversation to leave a reply.

Sign in to reply

Related topics