Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Shared Ownership

Sometimes you genuinely need multiple owners of the same data. Rust provides escape hatches for this - but they come with trade-offs.


Rc: Reference Counting (Single-Threaded)

Rc<T> (Reference Counted) allows multiple owners of the same data:

use std::rc::Rc;

fn main() {
    let data = Rc::new(vec![1, 2, 3]);
    
    let owner1 = Rc::clone(&data);  // Increment reference count
    let owner2 = Rc::clone(&data);  // Increment again
    
    println!("Reference count: {}", Rc::strong_count(&data));  // 3
    println!("All see same data: {:?}", owner2);
    
    drop(owner1);
    println!("After drop: {}", Rc::strong_count(&data));  // 2
}
// data is freed when the last Rc is dropped

Rc::clone doesn't clone the data - it just increments the count. Cheap!


The Catch: Rc is Immutable

Rc gives you shared ownership, but the data is immutable:

use std::rc::Rc;

fn main() {
    let data = Rc::new(vec![1, 2, 3]);
    
    data.push(4);  // ERROR: cannot borrow as mutable
    
    println!("{:?}", data);
}

Multiple owners means we can't have &mut - that would violate the borrowing rules!


RefCell: Runtime Borrow Checking

RefCell<T> moves borrow checking from compile time to runtime:

use std::cell::RefCell;

fn main() {
    let data = RefCell::new(vec![1, 2, 3]);
    
    // Borrow mutably at runtime
    data.borrow_mut().push(4);
    data.borrow_mut().push(5);
    
    // Borrow immutably to read
    let borrow1 = data.borrow();
    println!("{:?}", borrow1);  // [1, 2, 3, 4, 5]

    let borrow2 = data.borrow_mut();  // PANIC: already borrowed!
    
    // This is here to keep the first borrow alive
    println!("{:?}", borrow1);
}

This is a runtime panic, not a compile error. You've traded safety guarantees for flexibility.


Rc + RefCell: Shared Mutable State

Combine them for multiple owners with mutation:

use std::rc::Rc;
use std::cell::RefCell;

fn main() {
    let shared = Rc::new(RefCell::new(vec![1, 2, 3]));
    
    let owner1 = Rc::clone(&shared);
    let owner2 = Rc::clone(&shared);
    
    // Either owner can mutate
    owner1.borrow_mut().push(4);
    owner2.borrow_mut().push(5);
    
    println!("{:?}", shared.borrow());  // [1, 2, 3, 4, 5]
}

This is Rust's closest equivalent to C#'s reference semantics. Use sparingly - you're opting out of compile-time safety.


Arc: Thread-Safe Reference Counting

Rc is not thread-safe. For multi-threaded code, use Arc (Atomic Reference Counted):

use std::sync::Arc;
use std::thread;

fn main() {
    let data = Arc::new(vec![1, 2, 3]);
    
    let data_clone = Arc::clone(&data);
    let handle = thread::spawn(move || {
        println!("From thread: {:?}", data_clone);
    });
    
    println!("From main: {:?}", data);
    handle.join().unwrap();
}

Arc has slightly more overhead than Rc (atomic operations), so use Rc when you don't need thread safety.


Arc is Also Immutable

Just like Rc, Arc only gives you shared immutable access, because we need to protect against data races:

use std::sync::Arc;

fn main() {
    let data = Arc::new(vec![1, 2, 3]);
    
    data.push(4);  // ERROR: cannot borrow as mutable
}

For thread-safe mutation, you need a lock.


Mutex: Mutual Exclusion

Mutex<T> provides exclusive access across threads:

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];
    
    for _ in 0..10 {
        let counter = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            let mut num = counter.lock().unwrap();
            *num += 1;
        });
        handles.push(handle);
    }
    
    for handle in handles {
        handle.join().unwrap();
    }
    
    println!("Final count: {}", *counter.lock().unwrap());  // 10
}

lock() blocks until you have exclusive access. The lock is released when the guard is dropped.

NOTE: Unlike in C#, locks in Rust wrap the data to be protected by the lock. This ensures that you must hold the lock to access the data and that the lock will automatically be released when you're done.


RwLock: Multiple Readers OR One Writer

RwLock<T> allows many readers or one writer (like the borrow rules!):

use std::sync::{Arc, RwLock};
use std::thread;

fn main() {
    let data = Arc::new(RwLock::new(vec![1, 2, 3]));
    
    // Multiple readers can access simultaneously
    let data1 = Arc::clone(&data);
    let data2 = Arc::clone(&data);
    
    let reader1 = thread::spawn(move || {
        let read = data1.read().unwrap();
        println!("Reader 1: {:?}", *read);
    });
    
    let reader2 = thread::spawn(move || {
        let read = data2.read().unwrap();
        println!("Reader 2: {:?}", *read);
    });
    
    // Writer needs exclusive access
    {
        let mut write = data.write().unwrap();
        write.push(4);
        println!("Writer added 4");
    }
    
    reader1.join().unwrap();
    reader2.join().unwrap();
}

Use RwLock when reads are much more common than writes. Rust's immutable/mutable borrowing rules help enforce safe access patterns here. The compiler will not allow you to mutate data within an RwLock unless you hold the write lock.


The Hierarchy of Escape Hatches

NeedSingle-ThreadedMulti-Threaded
Shared ownershipRc<T>Arc<T>
Interior mutabilityRefCell<T>Mutex<T> / RwLock<T>
BothRc<RefCell<T>>Arc<Mutex<T>> / Arc<RwLock<T>>

When to Use These

Prefer normal ownership and borrowing. These types are escape hatches, not defaults.

Good uses:

  • Graph structures with cycles
  • Shared configuration across a system
  • Caches that multiple components read/write
  • Thread-safe shared state

Code smells:

  • Using Rc<RefCell<T>> everywhere
  • Fighting the borrow checker on every function
  • Runtime panics from RefCell violations

If you're reaching for these constantly, consider whether a different design (like arenas with indices) might work better.