rust-smart-pointers-rc-arc

Rc and Arc in Rust: Sharing Ownership Safely

  • 5 min

Rc<T> and Arc<T> are smart pointers for sharing ownership of the same data.

Rust normally requires that each value has a single owner. But it also gives us controlled exceptions for cases where multiple parts of the program need to point to the same data.

Imagine a TV in the living room of a shared house.

  • All roommates “own” the TV.
  • Anyone can turn it on and watch it.
  • When is the TV thrown away? Only when the last roommate moves out and the house is empty.

This concept is called Reference Counting. Rust offers us two types to handle it: Rc<T> and Arc<T>.

Rc<T>: Reference Counted (Single-threaded)

Rc<T> allows multiple parts of your code to own the same data on the Heap. It works by maintaining an internal counter:

When we create a new reference to the data, the counter goes up (+1).

When a reference goes out of scope (drop), the counter goes down (-1).

When the counter reaches zero, the data is deleted.

Example: Shared Lists

Imagine we want two lists that share the same tail.

  • List a: 5 -> 20 -> Nil
  • List b: 3 -> 20 -> Nil

The node 20 belongs to both a and b. With Box this would be impossible, because Box does not allow sharing ownership.

use std::rc::Rc;

enum List {
    Cons(i32, Rc<List>),
    Nil,
}

use crate::List::{Cons, Nil};

fn main() {
    // 1. Create the shared part (tail)
    // Wrap Nil in an Rc to start the chain
    let tail = Rc::new(Cons(20, Rc::new(Nil)));
    println!("Counter after creating tail: {}", Rc::strong_count(&tail)); // 1

    // 2. Create list 'a' that points to 'tail'
    // Rc::clone does NOT copy the data. It only increments the counter. It's very fast.
    let a = Cons(5, Rc::clone(&tail));
    println!("Counter after creating a: {}", Rc::strong_count(&tail)); // 2

    {
        // 3. Create list 'b' that ALSO points to 'tail'
        let b = Cons(3, Rc::clone(&tail));
        println!("Counter after creating b: {}", Rc::strong_count(&tail)); // 3
    } // Here 'b' dies. The counter goes down to 2.

    println!("Counter at end of main: {}", Rc::strong_count(&tail)); // 2
} // Here 'a' dies. Counter goes down to 1.
  // Here 'tail' (the original variable) dies. Counter goes down to 0 -> Memory is freed.
Copied!

Rc::clone(&tail) vs tail.clone() We could call tail.clone(), but in Rust it is convention to use Rc::clone(&tail). Why?

Because clone() sometimes implies a deep copy that can be slow. Rc::clone makes it visually clear that we are only incrementing a counter (a cheap operation) and not copying all the data.

Shared Access and Immutability

Rc<T> normally offers shared read-only access. If we are the sole owner, Rc::get_mut can provide mutable access; when there are multiple owners, we need another mechanism.

If you need to modify data inside an Rc, you will need to combine it with Interior Mutability (RefCell), which we will see in the next article.

Arc<T>: Atomic Reference Counted (Multi-threaded)

If you try to use Rc<T> to share data between multiple threads, the compiler will give you an error.

Rc is not thread-safe (!Send). Why? Because the operation of “adding 1” to the counter is not atomic. If two threads try to clone the Rc at the exact same nanosecond, the counter could become corrupted, causing memory leaks or a “double free”.

For this, we have Arc<T> (Atomic Reference Counted).

Its counting works like that of Rc, but it uses atomic primitives to modify the counter. This allows sharing ownership between threads if the contained type also meets the Send and Sync requirements; Arc alone does not make any internal mutation safe.

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

fn main() {
    // Create a shared data for threads
    let data = Arc::new(vec![1.0, 2.0, 3.0]);

    let mut handles = Vec::new();

    for _ in 0..3 {
        // Clone the atomic pointer to pass it to the thread
        let data_clone = Arc::clone(&data);

        handles.push(thread::spawn(move || {
            // Each thread has its own pointer to the same vector on the Heap
            println!("Read: {:?}", data_clone);
        }));
    }

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

Why not always use Arc?

If Arc is safer, why does Rc exist? For Performance.

Atomic operations are generally more expensive than incrementing an ordinary counter.

  • If the ownership never crosses a single thread, use Rc.
  • If you need to share between threads, pay the small cost and use Arc.

Comparison of Ownership Pointers

So far we have three ways to own data on the Heap:

PointerOwnershipThreadsMain Use
Box<T>UniqueTransferable if T: SendLarge data, recursion, moving ownership.
Rc<T>SharedNOT SafeGraph structures, multiple owners in a single thread.
Arc<T>SharedShareable if T allows itSharing ownership between threads.

A cycle formed only by strong references of Rc or Arc keeps the counter above zero and can leak memory. For links that should not express ownership, like a child’s pointer to its parent, we use Weak<T>.