Atomic Reference Counting
Atomic reference counting is the synchronization mechanism used by Arc<T> and sync::Weak<T> to track strong and weak reference counts concurrently across multiple CPU cores without locks or data races.
Strong and Weak Counter Lifecycle
An ArcInner<T> allocation maintains two AtomicUsize counters:
strongcount: Tracks the number of activeArc<T>handles. Whenstrongtransitions from1to0, the inner valueTis dropped in place (ptr::drop_in_place).weakcount: Tracks the number of activeWeak<T>handles, plus1collective implicit weak reference held by all active strongArc<T>handles combined. The underlyingArcInner<T>heap memory is only deallocated whenweaktransitions from1to0.
State 1: One Arc<T>, One Weak<T>
+-------------------------------------------+
| strong = 1 |
| weak = 2 (1 for all Arcs + 1 for Weak) |
| data = Initialized T |
+-------------------------------------------+
|
| Drop last Arc<T> (strong: 1 -> 0)
v
State 2: Zero Arc<T>, One Weak<T>
+-------------------------------------------+
| strong = 0 |
| weak = 1 (held by the remaining Weak) |
| data = Dropped (memory block stays!) |
+-------------------------------------------+
|
| Drop last Weak<T> (weak: 1 -> 0)
v
State 3: Heap block deallocated
Memory Ordering in Arc::clone and Arc::drop
To minimize CPU overhead while guaranteeing that no thread accesses T after its destructor starts, Arc<T> uses asymmetric C11/Rust atomic memory orderings:
| Operation | Atomic Instruction & Ordering | Why This Ordering Is Sufficient |
|---|---|---|
Arc::clone |
strong.fetch_add(1, Ordering::Relaxed) |
Creating a new handle from an existing live Arc<T> does not transfer new data; the caller already holds a valid reference |
Arc::drop (decrement) |
strong.fetch_sub(1, Ordering::Release) |
Ensures all reads and writes to T on the current thread happen-before the final thread drops T |
Arc::drop (when previous count was 1) |
atomic::fence(Ordering::Acquire) before drop_in_place |
Synchronizes with the Release decrements of all previously dropped Arc<T> handles across other threads |
Weak::upgrade |
Compare-and-swap (compare_exchange_weak) loop with Ordering::Acquire on success |
Prevents incrementing strong if it has already reached 0 on another thread |
Inspecting Strong and Weak Counts
Because another thread can concurrently clone or drop an Arc<T> immediately after Arc::strong_count or Arc::weak_count returns, these functions should only be used for diagnostics, logging, or assertions in quiescent states—never for manual soundness checks.
use std::sync::Arc;
fn main() {
let strong_a = Arc::new(String::from("payload"));
let weak_a = Arc::downgrade(&strong_a);
println!(
"with 1 Arc + 1 Weak -> strong: {}, weak: {}",
Arc::strong_count(&strong_a),
Arc::weak_count(&strong_a)
);
drop(strong_a);
println!("upgrade after strong drop: {:?}", weak_a.upgrade());
println!("weak_count via Weak: {}", weak_a.weak_count());
}
with 1 Arc + 1 Weak -> strong: 1, weak: 1
upgrade after strong drop: None
weak_count via Weak: 1
Counter Overflow Protection
If code repeatedly calls std::mem::forget(Arc::clone(&arc)) in a loop without dropping the clones, the strong counter could theoretically wrap around usize::MAX back to 0 and trigger a premature drop. Arc::clone guards against this by aborting the process if the strong count exceeds isize::MAX (2^63 - 1 on 64-bit platforms).
Performance Costs of Atomic Reference Counting
On modern multi-core CPUs, atomic read-modify-write instructions (lock xadd on x86_64, ldadd on AArch64) require exclusive ownership of the cache line holding ArcInner<T>. When many threads clone and drop handles to the same Arc<T> in a tight loop, cache-line bouncing across cores can dominate execution time. Passing borrowed &T references within a thread (or using thread::scope) avoids atomic counter traffic inside hot loops.