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.
Recipe
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.
Working Example
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::cloneincrements ref count, not deep clone of data.RwLockprotects innerConfigfor readers and writers.- Each thread gets its own
Archandle viamove.
Deep Dive
How It Works
- Reference counting:
Arc::cloneis cheap (atomic increment). Data is freed when the lastArcdrops. - Immutability of Arc itself: You cannot get
&mut TfromArc<T>without interior mutability (Mutex,RwLock, atomics). - Thread safety:
ArcisSend + SyncwhenT: Send + Sync. - Weak references:
Arc::downgradecreatesWeakto break cycles (callbacks, graphs).
Common Compositions
| 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 |
Rust Notes
// 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);- Do not confuse
Arc::clonewith cloning inner data. - For single-thread sharing, use
Rc(cheaper, notSend). - In async,
Arcis the default for shared app state in Axum handlers.
Gotchas
- Deep clone by mistake -
(*arc).clone()clones innerT, not theArc. Fix: UseArc::clone(&arc)for another handle. - Arc without interior mutability - Cannot mutate through
Arc<T>alone. Fix: AddMutex,RwLock, or rebuild-and-swap pattern. - Lock held in Arc across await - Async tasks need
Send; mutex guards are not. Fix: Usetokio::sync::Mutexand scoped blocks. - Memory leaks via Arc cycles - Parent holds
Arc<Child>and child holdsArc<Parent>. Fix: UseWeakon one side. - Too much shared state - Everything in one
Arc<Mutex<App>>serializes the world. Fix: Shard state or use actors/channels.
Alternatives
| 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 |
FAQs
Arc vs Rc?
Arc uses atomic ref counting (Send + Sync). Rc is single-threaded and faster when threads are not involved.
Is Arc::clone expensive?
It is an atomic increment - cheap compared to cloning large inner data, but avoid cloning in tight loops if profiling shows hot spots.
Can I get &mut from Arc?
Not directly. Use interior mutability or Arc::get_mut only when you have the sole remaining strong reference.
How does Axum use Arc?
Arc<AppState> is cloned per handler invocation - cheap pointer bump for shared pools and config.
Weak references?
Weak does not keep data alive. Upgrade with weak.upgrade() to temporary Arc when the object still exists.
Arc and Drop order?
Last Arc dropped runs Drop on inner T. Order across threads is non-deterministic unless you synchronize shutdown.
Can Arc hold a Mutex?
Yes - Arc<Mutex<T>> is the standard shared mutable pattern for threads and many sync services.
Copy vs Arc?
Arc is for heap-allocated shared data. Copy types like i32 do not need Arc unless wrapped in a larger shared struct.
Performance tip?
Reduce lock scope, shard Arc instances per partition, or use immutable Arc snapshots for read-heavy paths.
Testing Arc code?
Use Arc::strong_count in tests to verify handles are dropped, or inject traits to mock shared services.
Related
- Mutex & RwLock - interior mutability
- Send & Sync - Arc thread bounds
- Channels - alternative to shared Arc state
- Shared State in Async - Arc in Tokio
- Concurrency Best Practices - share carefully
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+.