Shared Ownership with Rc and Arc

Understand how Rc and Arc enable multiple owners for the same heap data in Rust, and why thread safety is the key difference between them

Rust's ownership rules give every value exactly one owner. That works for most code, but sometimes multiple parts of a program genuinely need to hold onto the same piece of data — a shared configuration object, a graph of connected nodes, or a cache that several components update. Deleting the data when the last owner goes away is the right behavior, but Rust's "one owner" rule doesn't let you model it directly.

Rc<T> and Arc<T> are smart pointers that lift this restriction. They let you share ownership of heap-allocated data by keeping a counter of how many references are still alive. When that count hits zero, the data is dropped. The difference between the two is thread safety: Rc is fast but only works inside a single thread, while Arc uses atomic operations so it can be safely shared across threads.

Previous coverage of Rc:

This page focuses on how Rc and Arc compare and when to choose each one. For a deeper look at Rc internals and the Weak pointer that prevents reference cycles, revisit Rc<T>, the Reference Counted Smart Pointer and the Weak<T> section.

Rc<T> for Shared Ownership in a Single Thread

When multiple parts of a single-threaded program need to keep the same data alive, Rc<T> (Reference Counted) is the tool to reach for. It allocates T on the heap and stores a non-atomic counter next to it. Every call to Rc::clone bumps that counter and gives you a new handle that points to the exact same heap allocation — no deep copying happens.

A good mental model is a group of people holding handles attached to the same suitcase. Each handle is an Rc<T>, and the suitcase stays on the ground as long as at least one person is holding it. When the last person lets go, the suitcase is dropped. The counter simply tracks the number of handles still in use.

use std::rc::Rc;
fn main() {
    let shared = Rc::new(String::from("hello"));
    println!("count: {}", Rc::strong_count(&shared)); // 1
    let a = Rc::clone(&shared);
    let b = Rc::clone(&shared);
    println!("count: {}", Rc::strong_count(&shared)); // 3
    // a and b both read the same String — no duplicate allocation
    println!("a: {}, b: {}", a, b);
}

When you clone an Rc, the counter goes up; when an Rc goes out of scope, it goes down. The inner value is dropped only when the count drops to zero. You can inspect the count at any time with Rc::strong_count. In the example above, three owners exist simultaneously, so the String stays alive until the last one is gone.

The fact that Rc is not thread-safe is built into the type system: Rc<T> implements neither Send nor Sync. That means the compiler will refuse to move an Rc into another thread. This is by design — Rc uses ordinary (non-atomic) integer increments for performance, and sharing a non-atomic counter across threads would be a data race.

Compile error: Rc across threads:

Attempting to use Rc with std::thread::spawn results in a compilation error. The compiler tells you that Rc<String> cannot be sent between threads safely, because it doesn't implement Send. This is the compiler protecting you from a real bug.

Arc<T> for Thread-Safe Shared Ownership

Arc<T> — Atomically Reference Counted — solves the same problem as Rc but across threads. Instead of ordinary integer operations, Arc uses atomic instructions (like fetch-add) to update its counter. These atomic operations are safe to perform from multiple threads simultaneously, so Arc<T> is Send and Sync whenever T is.

The trade-off is a small performance cost. Atomic operations are slightly more expensive than plain integer addition, so Rust makes the choice explicit: you use Arc only when you genuinely need cross-thread sharing.

use std::sync::Arc;
use std::thread;
fn main() {
    let shared = Arc::new(vec![1, 2, 3, 4, 5]);
    let mut handles = vec![];
    for i in 0..3 {
        let data = Arc::clone(&shared);
        handles.push(thread::spawn(move || {
            let sum: i32 = data.iter().sum();
            println!("Thread {i}: sum = {sum}");
        }));
    }
    for h in handles {
        h.join().unwrap();
    }
    // The original Arc is still usable after the threads finish
    println!("main thread still sees: {:?}", shared);
}

The pattern is identical to Rc: call Arc::clone to get a new handle that keeps the data alive. The difference is that each clone can then be moved into a separate thread, because Arc<T> implements Send. In this example, three threads all read the same Vec concurrently, and the allocation is freed only when the main thread's Arc and all thread Arcs have been dropped.

Everything still accessible after threads:

The main thread prints the vector after all spawned threads finish. This confirms that the Arc handles inside the threads kept the allocation alive until their work was done, and the main handle is still valid afterward.

Choosing Between Rc and Arc

The decision comes down to one question: does this data need to be accessed from more than one thread?

If your entire program runs on a single thread, or the shared data never crosses a thread boundary, use Rc. It gives you the same shared-ownership semantics with the lowest possible overhead.

If the data will be sent to another thread — for example, a worker thread pool processing a shared read-only configuration — use Arc. The compiler will stop you from accidentally using Rc in a multi-threaded context, so you can start with Rc and change to Arc only when the compiler tells you to.

PointerThread-safeReference count mechanismTypical use
Rc<T>NoNon-atomic integerGraphs, trees, shared caches in single-threaded code
Arc<T>YesAtomic operationsShared data across threads, multi-threaded caches, background tasks

Both Rc and Arc give you shared immutable access by default. If you need to mutate the inner value, you combine them with interior mutability tools: RefCell<T> for Rc (single-threaded) and Mutex<T> or RwLock<T> for Arc (multi-threaded). This is covered in the Interior Mutability Pattern section and the concurrency chapter that follows this one.

Sharing Mutable State Across Threads with Arc<Mutex<T>>

A common real-world pattern is Arc<Mutex<T>>. The Arc provides shared ownership across threads, and the Mutex provides synchronized mutable access so only one thread can modify the data at a time.

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);
        handles.push(thread::spawn(move || {
            let mut num = counter.lock().unwrap();
            *num += 1;
        }));
    }
    for h in handles {
        h.join().unwrap();
    }
    println!("Final count: {}", *counter.lock().unwrap());
}

Ten threads each increment the same i32. The Mutex ensures that only one thread increments at a time, preventing data races. The Arc keeps the Mutex alive until all threads and the main function have dropped their handles. This pattern appears so often that recognizing it is essential for writing concurrent Rust code.

Don't hold a Mutex guard across an await point:

In async Rust, holding a Mutex lock across an .await can lead to deadlocks. This is a separate topic, but the mental model of Arc<Mutex<T>> for thread-safe shared state remains the same.

Common Mistakes and Misconceptions

Using Rc in a multi-threaded context. The compiler will catch this and produce an error like "Rc<...> cannot be sent between threads safely". The fix is to change Rc to Arc. This is the most frequent confusion for newcomers who are used to reference counting being thread-safe in other languages.

Assuming Arc makes the inner data thread-safe. Arc makes the ownership mechanism thread-safe, but it does not add synchronization to the value it holds. An Arc<Vec<i32>> can be shared across threads for reading, but if multiple threads attempt to push to the Vec simultaneously, you still have a data race. You need a Mutex or RwLock to make mutation safe.

Cloning the inner value instead of the pointer. A beginner might write let a = shared.clone() where shared is an Rc<String>, expecting a new Rc handle. Because Rc dereferences to its inner type, clone() might call String::clone and produce a deep copy instead of just incrementing the reference count. Always use Rc::clone(&shared) or Arc::clone(&shared) to be explicit about what you're cloning.

Building reference cycles. Two Rcs pointing at each other will never see their strong counts drop to zero, causing a memory leak. The solution is Weak<T>, which is covered in detail in the Weak<T> section. The same issue applies to Arc with std::sync::Weak.

Cycles silently leak memory:

A cycle of Rc or Arc strong references does not trigger a panic — it just prevents deallocation forever. If your program's memory usage grows monotonically, check for reference cycles in your graph data structures.

Summary

Rc and Arc break Rust's single-owner constraint without breaking memory safety. Rc is the efficient choice when all sharing happens within a single thread; Arc adds atomic operations so you can share ownership across thread boundaries. The syntax is nearly identical, and the compiler will tell you which one is required.

The crucial insight is that shared ownership does not equal shared mutability. An Arc alone lets many threads read the same data, but coordinated mutation still needs locks.