How to share ownership between threads with Arc

Share ownership between threads in Rust by wrapping data in Arc for reference counting and Mutex for thread-safe mutability.

When threads need to share a value

You're building a concurrent image processor. The main thread loads a color palette from a file. Three worker threads need to apply filters using that palette. You try to move the palette into the thread closures. The compiler rejects you because you can't move the same value three times. You try to clone the palette. Now you have three copies. Updating the palette in one thread doesn't update the others, and you've wasted memory duplicating data that should be shared.

You need a way to share a single allocation across threads without copying the data itself. Rust's ownership rules prevent data races by default, but they also block this pattern. Arc<T> bridges the gap. It allows multiple threads to own the same data safely. The name stands for Atomic Reference Counted. It wraps a value on the heap and tracks how many threads hold a reference to it. When the last reference drops, the value is cleaned up.

Atomic reference counting

Arc<T> works like Rc<T>, but with a crucial difference: the reference counter is atomic. Rc uses a plain integer for the count. That works fine in a single thread. Across threads, a plain integer is not safe. Two threads might read the count, decrement it, and write it back simultaneously, losing a decrement. The counter could drop to zero while a thread still holds a reference, causing a use-after-free bug.

Arc uses atomic operations to update the counter. Atomic operations guarantee that reads and writes happen indivisibly. Even if two threads try to decrement the counter at the exact same nanosecond, the hardware ensures the result is correct. This adds a small performance cost compared to Rc, but it guarantees correctness. You pay for thread safety only when you need it.

Think of the counter like a digital turnstile at a stadium. A plain counter is like a person counting heads by eye. If two people walk through at once, the counter might miss one. An atomic counter is like a sensor that registers each person individually, no matter how fast they move. The count stays accurate under pressure.

Minimal example

Here is how you share immutable data across threads. The data lives on the heap. Each thread gets an Arc pointing to the same allocation. Cloning the Arc bumps the counter. It does not copy the data.

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

/// Share a vector of data across multiple threads.
fn main() {
    // Create the shared value on the heap with an atomic counter.
    let shared_data = Arc::new(vec![1, 2, 3]);

    let mut handles = vec![];

    // Spawn three threads, each getting a handle to the same data.
    for _ in 0..3 {
        // Clone the Arc, not the vector. This bumps the atomic counter.
        let data_clone = Arc::clone(&shared_data);
        let handle = thread::spawn(move || {
            // Read the data. No mutation here, so no Mutex needed.
            println!("Thread sees: {:?}", data_clone);
        });
        handles.push(handle);
    }

    // Wait for all threads to finish.
    for handle in handles {
        handle.join().unwrap();
    }

    // The data is still alive because the main thread holds an Arc.
    println!("Main sees: {:?}", shared_data);
}

Community convention prefers Arc::clone(&data) over data.clone(). Both compile and do the same thing. The explicit form signals to readers that you are cloning the reference count, not the underlying data. data.clone() looks like it might copy the vector, which can mislead someone scanning the code. Use the explicit form to avoid ambiguity.

The counter keeps the memory alive. Zero means free.

What happens under the hood

When you call Arc::new, Rust allocates memory for the value and a control block containing the atomic counter. The counter starts at one. The Arc struct holds a pointer to this allocation. When you call Arc::clone, Rust creates a new Arc struct pointing to the same allocation and atomically increments the counter. Each Arc that goes out of scope atomically decrements the counter. When the counter reaches zero, the allocation is freed.

The atomic operations use CPU instructions like compare-and-swap. These instructions are more expensive than plain integer arithmetic. They may cause cache line invalidations across cores. If you are cloning Arc in a tight loop, the overhead can add up. Profile before optimizing. In most applications, the cost is negligible compared to I/O or computation.

Trust the atomic counter. It handles the concurrency for you.

Sharing mutable state

Arc<T> only gives shared read access. You cannot mutate the data through an Arc alone. Rust's type system prevents data races by enforcing exclusive access for mutation. If you need to mutate shared data, you must combine Arc with a synchronization primitive like Mutex<T> or RwLock<T>.

Arc handles the sharing. Mutex handles the mutation. The Mutex ensures only one thread can access the data at a time. The Arc ensures the data stays alive as long as any thread holds a reference.

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

/// Increment a shared counter from multiple threads.
fn main() {
    // Wrap the counter in Arc for shared ownership and Mutex for interior mutability.
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        // Clone the Arc to move ownership into the thread closure.
        let counter_clone = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            // Lock the mutex to gain exclusive access to the counter.
            let mut num = counter_clone.lock().unwrap();
            *num += 1;
        });
        handles.push(handle);
    }

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

    // Print the final count.
    println!("Final count: {}", *counter.lock().unwrap());
}

The lock() call blocks the thread until the mutex is available. It returns a guard that holds the lock. The guard implements DerefMut, so you can access the data like a mutable reference. When the guard drops, the lock is released. Keep the guard alive only as long as you need the lock. Holding the lock across a yield or a long computation blocks other threads and hurts performance.

Lock the mutex, do the work, drop the guard. Keep the critical section tight.

Pitfalls and compiler errors

Arc<T> imposes trait bounds on T. The type inside must be Send and Sync. Send means the value can be transferred to another thread. Sync means the value can be shared between threads. If you try to wrap a type that isn't Send or Sync, the compiler rejects you with E0277 (trait bound not satisfied).

Rc<T> is not Send because its counter isn't atomic. You cannot share an Rc across threads. If you try to put an Rc inside an Arc, the compiler stops you. You must use Arc for thread-safe sharing.

use std::rc::Rc;
use std::sync::Arc;

fn main() {
    // This fails to compile. Rc is not Send.
    let bad = Arc::new(Rc::new(42));
}

The compiler error tells you that Rc<i32> cannot be shared between threads safely. Replace Rc with Arc to fix this.

Watch out for mutex poisoning. If a thread panics while holding a lock, the mutex marks itself as poisoned. Subsequent lock() calls return a PoisonError. Calling unwrap() on a poisoned mutex panics again, which can cascade failures. In production code, handle the error. PoisonError gives you access to the guard even if poisoned, so you can inspect the state or recover. Decide whether to abort or continue based on your application's requirements.

Reference cycles cause memory leaks. If struct A holds an Arc<B> and struct B holds an Arc<A>, the counters never reach zero. The memory leaks. Use Weak references to break cycles. Weak holds a non-owning reference. It doesn't increment the counter. You can upgrade a Weak to an Arc if the value is still alive. upgrade() returns Option<Arc<T>>. It returns None if the value has been dropped.

Break the cycle with Weak. If the counter never drops, the memory never frees.

When to use Arc

Use Arc<T> when you need to share immutable data across multiple threads and the data lives as long as the threads. Use Arc<Mutex<T>> when multiple threads need to read and write the same data, and you need exclusive access for writes. Use Arc<RwLock<T>> when you have many readers and few writers, and the read operations are expensive enough to justify the overhead of a read-write lock. Use Rc<T> when you are in a single-threaded context and want to share ownership without the cost of atomic operations. Use Send channels when threads need to transfer ownership of data rather than share it; sharing via Arc can lead to contention and complexity that a message-passing design avoids.

Reach for channels before Arc<Mutex<T>>. Shared state is hard; messages are easier.

Where to go next