Smart Pointers

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:

  1. strong count: Tracks the number of active Arc<T> handles. When strong transitions from 1 to 0, the inner value T is dropped in place (ptr::drop_in_place).
  2. weak count: Tracks the number of active Weak<T> handles, plus 1 collective implicit weak reference held by all active strong Arc<T> handles combined. The underlying ArcInner<T> heap memory is only deallocated when weak transitions from 1 to 0.
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.