Concurrency Fundamentals

Shared Ownership

In concurrent programming, shared ownership allows multiple threads to co-own the same heap-allocated value when no single thread or lexical scope has a lifetime guaranteed to outlive all others. Rust models multi-threaded shared ownership through std::sync::Arc<T> (Atomically Reference Counted).

Choosing Between Scoped Borrowing (&T) and Shared Ownership (Arc<T>)

Before wrapping data in Arc<T>, check whether the threads have a bounded lexical lifetime:

Do the spawned threads finish before the parent function returns?
  |
  +-- YES ---> Use `std::thread::scope` and borrow `&T` directly!
  |            (Zero heap allocation, zero atomic reference-count traffic)
  |
  +-- NO  ---> Threads outlive the creating function (`thread::spawn` / `tokio::spawn`):
               Use `Arc<T>` so the last finishing thread drops `T`!
Dimension Scoped Borrowing (std::thread::scope + &T) Shared Ownership (std::thread::spawn + Arc<T>)
Thread lifetime Joined before thread::scope returns Unbounded ('static); threads can outlive creator
Heap allocation None required (T can live on caller's stack) 1 heap allocation (ArcInner<T>)
Clone / drop overhead 0 (&T is a plain Copy pointer) Atomic fetch_add on clone, fetch_sub on drop
Required trait bounds on T T: Sync T: Send + Sync + 'static

The Shadow-Clone Idiom Before move ||

Because a move || closure passed to thread::spawn moves captured variables into the spawned thread, moving the original Arc<T> directly into the first thread would consume it before the second thread can use it.

The idiomatic pattern is shadow-cloning the Arc<T> inside an inner scope or loop iteration (let data = Arc::clone(&data);) right before thread::spawn(move || ...):

use std::sync::Arc;
use std::thread;

#[derive(Debug)]
struct Dictionary {
    words: Vec<&'static str>,
}

fn main() {
    let dict = Arc::new(Dictionary {
        words: vec!["rust", "concurrency", "ownership", "atomic"],
    });

    let mut handles = Vec::new();

    for min_len in [4, 7, 10] {
        // Shadow `dict` with an atomic reference-counted clone for this thread:
        let dict = Arc::clone(&dict);
        handles.push(thread::spawn(move || {
            let matches: Vec<&str> = dict
                .words
                .iter()
                .copied()
                .filter(|w| w.len() >= min_len)
                .collect();
            (min_len, matches)
        }));
    }

    for handle in handles {
        let (min_len, matches) = handle.join().unwrap();
        println!("words with len >= {:>2}: {:?}", min_len, matches);
    }

    println!("remaining strong_count on main: {}", Arc::strong_count(&dict));
}
words with len >=  4: ["rust", "concurrency", "ownership", "atomic"]
words with len >=  7: ["concurrency", "ownership"]
words with len >= 10: ["concurrency"]
remaining strong_count on main: 1

Copy-on-Write Shared State with Arc::make_mut

When threads share an Arc<Config> that is read 99.9% of the time and only occasionally updated, calling Arc::make_mut(&mut local_arc) clones the underlying Config (T: Clone) into a fresh Arc<Config> if strong_count > 1 (or mutates it in place if strong_count == 1), enabling lock-free snapshot updates without wrapping reads in a Mutex.