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.