Why shared ownership exists in Rust

Rust’s ownership model encourages a single clear owner for each value. That works well for most data, but some structures naturally need multiple owners:

  • trees with parent and child references
  • caches shared across components
  • application state passed into many tasks
  • DAGs and graphs
  • configuration or metadata reused throughout a program

In these cases, moving the value into one place is too restrictive, and cloning the entire object may be too expensive or semantically wrong. Reference counting solves this by keeping the data in a heap allocation and tracking how many owners exist.

When the last owner disappears, the allocation is freed automatically.

Rc<T>: shared ownership in single-threaded code

Rc<T> stands for reference-counted. It increments a non-atomic counter on clone, which makes it lightweight, but also not thread-safe. Because of that, Rc<T> cannot be sent across threads.

Use Rc<T> when:

  • the data is shared within one thread
  • cloning the pointer should be cheap
  • you want immutable shared access
  • you are building recursive or graph-like structures in a single-threaded context

Basic example

use std::rc::Rc;

fn main() {
    let config = Rc::new(String::from("production"));

    let a = Rc::clone(&config);
    let b = Rc::clone(&config);

    println!("mode = {}", config);
    println!("strong_count = {}", Rc::strong_count(&config));
    drop(a);
    println!("strong_count after drop = {}", Rc::strong_count(&config));
    drop(b);
}

Notice that Rc::clone(&config) clones the pointer, not the underlying string. This distinction matters: the string is allocated once, and all owners point to the same allocation.

Why Rc::clone is preferred over .clone()

Both work, but Rc::clone(&value) makes the intent explicit: you are cloning the reference count, not the inner data. In code reviews, this is easier to recognize and less misleading than a generic .clone() call.

Arc<T>: shared ownership across threads

Arc<T> stands for atomic reference-counted. It uses atomic operations to update the count safely across threads. That makes it slightly more expensive than Rc<T>, but it is the standard choice for concurrent shared ownership.

Use Arc<T> when:

  • the value must be shared between threads or async tasks
  • you need cheap cloning of a shared handle
  • the shared data is immutable, or protected by synchronization primitives

Basic example

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

fn main() {
    let shared = Arc::new(vec![1, 2, 3]);

    let mut handles = Vec::new();

    for _ in 0..3 {
        let data = Arc::clone(&shared);
        handles.push(thread::spawn(move || {
            println!("len = {}", data.len());
        }));
    }

    for handle in handles {
        handle.join().unwrap();
    }
}

Here, each thread receives its own Arc handle, but all of them point to the same vector. Because the vector is only read, no additional synchronization is needed.

Choosing between Rc, Arc, and cloning the data

A common mistake is to use shared ownership too early. Sometimes cloning the actual value is simpler and faster than introducing reference counting. Other times, shared ownership is the right abstraction.

SituationBest choiceWhy
Single-threaded shared read accessRc<T>Cheap, simple, no atomic overhead
Multi-threaded shared read accessArc<T>Thread-safe reference counting
Shared mutable state in one threadRc<RefCell<T>>Runtime borrow checking for mutation
Shared mutable state across threadsArc<Mutex<T>> or Arc<RwLock<T>>Synchronization plus shared ownership
One-off independent copyT::clone()Simpler than shared ownership

A good rule: use Rc or Arc only when multiple owners are genuinely needed. If a function can take ownership or borrow a value directly, prefer that first.

Combining shared ownership with mutation

Rc<T> and Arc<T> give you shared ownership, but not shared mutation by themselves. The inner value is typically accessed immutably through deref. If you need mutation, combine them with interior mutability.

Single-threaded mutation: Rc<RefCell<T>>

RefCell<T> enforces Rust’s borrowing rules at runtime instead of compile time. This is useful when the compiler cannot prove the borrowing pattern, but you know it is safe.

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

fn main() {
    let counter = Rc::new(RefCell::new(0));

    let a = Rc::clone(&counter);
    let b = Rc::clone(&counter);

    *a.borrow_mut() += 1;
    *b.borrow_mut() += 1;

    println!("count = {}", counter.borrow());
}

This pattern is common in UI trees, test doubles, and graph structures where nodes need to update shared state.

Multi-threaded mutation: Arc<Mutex<T>>

For concurrent mutation, use Mutex<T> or RwLock<T> inside Arc<T>.

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

fn main() {
    let counter = Arc::new(Mutex::new(0));
    let mut handles = Vec::new();

    for _ in 0..4 {
        let c = Arc::clone(&counter);
        handles.push(thread::spawn(move || {
            let mut guard = c.lock().unwrap();
            *guard += 1;
        }));
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("final = {}", *counter.lock().unwrap());
}

This is the canonical pattern for shared mutable state in threaded Rust. The Mutex protects the value, while Arc ensures all threads own the same allocation.

Building graph-like structures safely

Reference counting is especially useful for structures with multiple parents or back-references. A classic example is a tree node with a parent pointer and children list.

However, using Rc alone for both directions creates a cycle: the parent owns the child, and the child owns the parent, so neither count reaches zero. To avoid leaks, use Weak<T> for non-owning back-references.

Example: parent pointers with Weak

use std::cell::RefCell;
use std::rc::{Rc, Weak};

#[derive(Debug)]
struct Node {
    name: String,
    parent: RefCell<Weak<Node>>,
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    let root = Rc::new(Node {
        name: "root".into(),
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(Vec::new()),
    });

    let child = Rc::new(Node {
        name: "child".into(),
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(Vec::new()),
    });

    *child.parent.borrow_mut() = Rc::downgrade(&root);
    root.children.borrow_mut().push(Rc::clone(&child));

    println!("root strong = {}", Rc::strong_count(&root));
    println!("root weak = {}", Rc::weak_count(&root));
}

Weak<T> does not contribute to the strong count. It can be upgraded temporarily to an Rc<T> if the value is still alive. This is the standard way to prevent cycles in shared ownership graphs.

Avoiding common pitfalls

1. Reference cycles

A cycle means the allocation never gets freed. This is the most important risk with shared ownership.

Use Weak<T> for:

  • parent pointers
  • observer links
  • caches that should not keep objects alive
  • any back-reference that should not own the target

2. Overusing Arc

Arc is not a universal replacement for borrowing. It adds atomic overhead and can hide ownership boundaries. If a function only needs read access for the duration of a call, prefer &T or &mut T.

3. Assuming Arc makes inner data thread-safe

Arc<T> only makes the pointer safe to clone across threads. The inner T must still be safe to share. For mutable access, wrap it in Mutex, RwLock, or another synchronization primitive.

4. Cloning the inner data accidentally

If you write (*arc).clone(), you may clone the underlying value rather than the pointer. That can be correct, but it is a different operation. Be deliberate about whether you want shared ownership or a deep copy.

Practical design guidelines

When designing APIs, shared ownership should be a conscious choice, not a default.

Prefer these patterns

  • Accept &T for temporary read access
  • Accept &mut T for exclusive mutation
  • Use Rc<T> for single-threaded shared structures
  • Use Arc<T> for cross-thread shared structures
  • Add RefCell, Mutex, or RwLock only when mutation is truly shared
  • Use Weak<T> to break ownership cycles

A useful mental model

Think of Rc and Arc as shared handles to a heap allocation. Cloning the handle is cheap; cloning the data is not implied. The handle decides who keeps the allocation alive, while the inner type decides whether mutation is possible and how it is synchronized.

Summary

Rc and Arc solve the same conceptual problem: multiple owners for one allocation. The difference is thread safety. Rc is ideal for single-threaded shared structures, while Arc is the standard tool for concurrent sharing.

For mutation, pair them with the appropriate interior mutability primitive: RefCell for single-threaded code, Mutex or RwLock for multi-threaded code. And whenever you build back-references, use Weak to avoid cycles.

Used carefully, these types let you model complex ownership relationships without abandoning Rust’s safety guarantees.

Learn more with useful resources