Arc for Shared State
Arc (atomically reference counted) lets multiple threads share ownership of the same heap allocation. Combine it with Mutex, RwLock, or atomics when threads need the same state.
Search across all documentation pages
Arc (atomically reference counted) lets multiple threads share ownership of the same heap allocation. Combine it with Mutex, RwLock, or atomics when threads need the same state.
Quick-reference recipe card - copy-paste ready.
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let shared = Arc::new(Mutex::new(0));
let handle = thread::spawn({
let shared = Arc::clone(&shared);
move || {
*shared.lock().unwrap() += 1;
}
});
handle.join().unwrap();
}When to reach for this: Multiple threads need long-lived access to the same configuration, cache, or counter.
use std::sync::{Arc, RwLock};
use std::thread;
struct Config {
api_url: String,
timeout_ms: u64,
}
fn main() {
let config = Arc::new(RwLock::new(Config {
api_url: "https://api.example.com".into(),
timeout_ms: 5000,
}));
let reader = {
let config = Arc::clone(&config);
thread::spawn(move || {
let cfg = config.read().unwrap();
println!("timeout = {}", cfg.timeout_ms);
})
};
let writer = {
let config = Arc::clone(&config);
thread::spawn(move || {
let mut cfg = config.write().unwrap();
cfg.timeout_ms = 3000;
})
};
reader.join().unwrap();
writer.join().unwrap();
}What this demonstrates:
Arc::clone increments ref count, not deep clone of data.RwLock protects inner Config for readers and writers.Arc handle via move.Arc::clone is cheap (atomic increment). Data is freed when the last Arc drops.&mut T from Arc<T> without interior mutability (Mutex, RwLock, atomics).Arc is Send + Sync when T: Send + Sync.Arc::downgrade creates Weak to break cycles (callbacks, graphs).| Pattern | Use case |
|---|---|
Arc<Mutex<T>> | Shared mutable struct |
Arc<RwLock<T>> | Read-heavy shared data |
Arc<T> (immutable) | Shared config after init |
Arc<AtomicUsize> | Simple shared counter |
// Cheap clone - prefer Arc::clone(&x) over x.clone() for clarity
let a = Arc::new(vec![1, 2, 3]);
let b = Arc::clone(&a);
// Weak breaks Arc cycles
use std::sync::Weak;
let weak: Weak<Vec<i32>> = Arc::downgrade(&a);Arc::clone with cloning inner data.Rc (cheaper, not Send).Arc is the default for shared app state in Axum handlers.(*arc).clone() clones inner T, not the Arc. Fix: Use Arc::clone(&arc) for another handle.Arc<T> alone. Fix: Add Mutex, RwLock, or rebuild-and-swap pattern.Send; mutex guards are not. Fix: Use tokio::sync::Mutex and scoped blocks.Arc<Child> and child holds Arc<Parent>. Fix: Use Weak on one side.Arc<Mutex<App>> serializes the world. Fix: Shard state or use actors/channels.| Alternative | Use When | Don't Use When |
|---|---|---|
Rc | Single-thread sharing | Data crosses thread boundaries |
| Channels | Clear producer/consumer roles | Many readers need latest snapshot |
thread_local! | Per-thread caches | Global consistency required |
| Database / external store | Durability and multi-process | Latency-sensitive in-memory only |
Arc uses atomic ref counting (Send + Sync). Rc is single-threaded and faster when threads are not involved.
It is an atomic increment - cheap compared to cloning large inner data, but avoid cloning in tight loops if profiling shows hot spots.
Not directly. Use interior mutability or Arc::get_mut only when you have the sole remaining strong reference.
Arc<AppState> is cloned per handler invocation - cheap pointer bump for shared pools and config.
Weak does not keep data alive. Upgrade with weak.upgrade() to temporary Arc when the object still exists.
Last Arc dropped runs Drop on inner T. Order across threads is non-deterministic unless you synchronize shutdown.
Yes - Arc<Mutex<T>> is the standard shared mutable pattern for threads and many sync services.
Arc is for heap-allocated shared data. Copy types like i32 do not need Arc unless wrapped in a larger shared struct.
Reduce lock scope, shard Arc instances per partition, or use immutable Arc snapshots for read-heavy paths.
Use Arc::strong_count in tests to verify handles are dropped, or inject traits to mock shared services.
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.
Reviewed by Chris St. John·Last updated Jul 16, 2026