Arc<T> in Rust: Thread-Safe Shared Ownership Guide

Max WellsMax WellsFounder of Rustify

TL;DR: Arc<T> (Atomically Reference Counted) is a smart pointer that enables multiple owners of the same heap-allocated value across threads. Each clone of an Arc increments an atomic counter; when the last clone is dropped, the value is freed. Arc<T> provides shared read-only access, to mutate the inner value, combine with Mutex<T> or RwLock<T>. For single-threaded shared ownership, use Rc<T> (no atomic overhead). Arc is the standard solution for sharing state in async Rust and multi-threaded applications.


What Is Arc<T> in Rust?

Arc<T> allows multiple parts of a program to share ownership of the same heap-allocated value, the value is freed only when the last Arc clone is dropped.

use std::sync::Arc;
 
fn main() {
    let data = Arc::new(vec![1, 2, 3, 4, 5]);
 
    let data1 = Arc::clone(&data); // increments ref count; no data copy
    let data2 = Arc::clone(&data); // increments ref count again
 
    println!("Reference count: {}", Arc::strong_count(&data)); // 3
 
    drop(data1); // decrements count to 2
    drop(data2); // decrements count to 1
    // data dropped here; count reaches 0, Vec is freed
}

Arc::clone is cheap, it only increments an atomic integer. It does not copy the data. The naming Arc::clone(&x) (vs x.clone()) is idiomatic to make it clear you are cloning the pointer, not the data.


How Do You Share State Across Threads With Arc?

Wrap the value in Arc and clone it for each thread. The data lives until the last thread drops its clone.

use std::sync::Arc;
use std::thread;
 
fn main() {
    let config = Arc::new(vec!["host=localhost", "port=8080"]);
    let mut handles = vec![];
 
    for i in 0..4 {
        let config = Arc::clone(&config); // clone the Arc, not the Vec
        let handle = thread::spawn(move || {
            println!("Thread {i} sees: {:?}", config);
        });
        handles.push(handle);
    }
 
    for handle in handles {
        handle.join().unwrap();
    }
}

This works because Arc<T> implements Send when T: Send + Sync, the compiler guarantees it is safe to transfer across thread boundaries.


How Do You Mutate Shared State With Arc<Mutex<T>>?

Arc provides shared ownership; Mutex provides exclusive mutable access. Combined, they are the standard pattern for shared mutable state 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(); // acquire lock
            *num += 1;
            // lock released when `num` goes out of scope
        });
        handles.push(handle);
    }
 
    for handle in handles {
        handle.join().unwrap();
    }
 
    println!("Final count: {}", *counter.lock().unwrap()); // 10
}

.lock() blocks until the mutex is available and returns a MutexGuard<T>, a smart pointer that releases the lock when dropped.


What Is the Difference Between Arc and Rc?

Rc<T> uses non-atomic reference counting, cheaper, but not thread-safe. Arc<T> uses atomic operations, safe across threads but with a small overhead.

Rc<T>Arc<T>
Thread-safe
Reference count operationsNon-atomicAtomic
Performance overheadMinimalSmall (atomic op)
Use caseSingle-threaded shared ownershipMulti-threaded or async code
Implements Send✅ (when T: Send + Sync)

If you try to send an Rc<T> to another thread, the compiler rejects it, Rc is not Send. This is a compile-time safety guarantee, not a runtime check.


How Is Arc Used in Async Rust?

In Tokio-based applications, Arc is the standard way to share state across async tasks, often wrapped around database pools, configuration, and caches.

use std::sync::Arc;
use tokio::sync::RwLock; // tokio's async-aware RwLock
 
#[derive(Clone)]
struct AppState {
    db_pool: Arc<sqlx::PgPool>,
    cache:   Arc<RwLock<std::collections::HashMap<String, String>>>,
}
 
// In Axum: AppState implements Clone via Arc::clone; cheap to share
async fn handler(state: axum::extract::State<AppState>) {
    let cache = state.cache.read().await; // async read lock
    // ...
}

Use tokio::sync::Mutex and tokio::sync::RwLock instead of std::sync equivalents in async code, the std versions block the thread, which defeats the purpose of async.


Frequently Asked Questions

Arc prevents use-after-free and double-free. It does not prevent all memory leaks, a reference cycle (Arc<A> contains Arc<B>, which contains Arc<A>) will keep both alive forever. Break cycles with Weak<T> (a non-owning reference that does not increment the strong count).

Mutex gives exclusive access (one writer or one reader at a time). RwLock allows concurrent readers but exclusive writers. Use RwLock for read-heavy workloads. Use Mutex when writes are as common as reads, or to avoid deadlock complexity.

Arc<T> is only Send + Sync when T: Send + Sync. If T contains a type that is not thread-safe (like Rc<U> or a raw pointer), the compiler rejects the Arc. Fix by using thread-safe alternatives (Arc instead of Rc, etc.).

Weak<T> is a non-owning reference to Arc data. It does not prevent the value from being freed. Use .upgrade() to attempt to get an Arc<T>, returns None if the value has been dropped. Use Weak to break reference cycles.


Sources


  • Mutex: Paired with Arc for shared mutable state
  • Ownership: Arc extends ownership to multiple simultaneous owners
  • Async/Await: Arc is essential for sharing state across async tasks
  • Box: Box for single-owner heap allocation; Arc for shared
  • RwLock: Arc<RwLock<T>> is the standard pattern when reads vastly outnumber writes
  • Spawn: Arc is the usual way to share state safely across spawned async tasks

Keep Reading

Ready to Land a $120k+ Rust Job in the US or Europe?