Rust / the practical course

03 / Concurrency

Interior mutability and shared ownership

Choose between reference counting, runtime borrow checks, and locks.

50 min + practiceRust 1.98.1 · Edition 2024

By Robert DeVore · Download Markdown

Shared does not always mean unchanging

Ordinary data behind &T cannot be mutated while the shared reference's contract applies. Interior-mutability types let you change data through a shared reference under defined rules. They rely on UnsafeCell internally, but each safe abstraction supplies its own rules. UnsafeCell itself does not prevent data races or allow competing &mut references.

Run the example

cargo run --locked --example 15_interior
use std::{cell::RefCell, rc::Rc};
fn main() {
    let data = Rc::new(RefCell::new(vec![1]));
    let other = Rc::clone(&data);
    {
        let mut guard = other.borrow_mut();
        guard.push(2);
        assert!(data.try_borrow().is_err());
    }
    assert_eq!(*data.borrow(), [1, 2]);
    assert_eq!(Rc::strong_count(&data), 2);
}

View the tested source

Rc gives two owning handles to the same allocation. RefCell tracks borrows at runtime. While the mutable guard exists, try_borrow() reports a conflict. After that guard is dropped, the shared borrow succeeds. Leaving the block drops the guard and ends its borrow.

borrow() and borrow_mut() panic on a runtime conflict; their try_ variants return an error. Use the try_ variants when your program should handle a borrow conflict rather than panic. A runtime check does not mean the compiler has stopped enforcing all safety: the guard types and library implementation cooperate to preserve access rules.

Pick the narrowest mechanism

Cell<T> can replace a value without handing out ordinary references to its interior. RefCell<T> supports checked borrows within a thread. Mutex<T> coordinates access across threads. Rc<T> is not a thread-safe reference counter; Arc<T> is, but that does not make every T thread-safe. Arc<RefCell<T>> is not a substitute for a synchronized mutation abstraction.

Reference-counted cycles can leak memory. Use weak references or redesign ownership where a graph needs back-links. A leak is a resource bug even when it does not violate memory safety. Rust's safety model does not imply every resource is eventually reclaimed.

Apply this to the course project

The event counter owns its summary and reads records sequentially. It does not need a reference-counted mutable summary. Passing &mut Summary to a function already states the intended exclusive update. Add a cell or lock only when a real relationship requires shared access with mutation.

If a compiler error appears because two components both want ownership, first consider transferring ownership at a message boundary. If they only read immutable data, an ordinary borrow or Arc<T> may be enough. Each mechanism adds behavior you need to understand and test.

Exercise

Keep the mutable RefCell guard alive and call try_borrow from the other handle. Assert failure. Drop the guard explicitly and assert success. Explain why cloning the Rc does not create a second vector.

Solution and acceptance check

The first attempt returns Err; the second sees [1, 2]. Both handles refer to the same cell and its borrow state. Rc::clone increases the owning handle count. To copy the data itself, you would need to clone the vector separately.

Sources: RefCell, UnsafeCell, and Rc.

Find a lesson