Step 3 of 7
Arc: shared ownership across threads
A channel moves data from one thread to another. Sometimes many threads need to read the same data: a price list, a config, a dictionary. Cloning it for every thread wastes memory, and scoped threads only work when all threads finish inside one function. For threads started with thread::spawn, you share data with Arc<T>.
You met Arc briefly in the last module. It's the thread-safe version of Rc (module 29): Arc::new(value) puts the value on the heap next to a reference count. Arc::clone(&a) makes another pointer to the same value and adds one to the count; no data is copied. When the last Arc is dropped, the value is dropped. The count is updated with atomic CPU instructions, so Arc<T> can be sent to and shared between threads (as long as T itself is Send and Sync).
use std::collections::HashMap;
use std::sync::Arc;
use std::thread;
fn main() {
let prices: HashMap<&str, u32> = HashMap::from([("kelp", 3), ("shell", 12), ("pearl", 250)]);
let prices = Arc::new(prices);
let orders = vec![vec!["kelp", "kelp", "shell"], vec!["pearl"], vec!["shell", "kelp"]];
let mut handles = Vec::new();
for order in orders {
let prices = Arc::clone(&prices);
handles.push(thread::spawn(move || order.iter().map(|item| prices[item]).sum::<u32>()));
}
for (i, handle) in handles.into_iter().enumerate() {
println!("order {i}: {}", handle.join().unwrap());
}
println!("owners of the price table now: {}", Arc::strong_count(&prices));
}
order 0: 18
order 1: 250
order 2: 15
owners of the price table now: 1
- Each thread gets its own
Arc(a pointer), moved into its closure. All of them point at oneHashMap. Arc::strong_countshows how manyArcs point at the value. After the joins, the threads' clones have been dropped, so onlymain's is left.Arc::clone(&prices)andprices.clone()do the same thing. Many teams preferArc::clone(&x)because it makes clear that only a pointer is copied, not the whole table.
Arc is read-only
Arc<T> hands out shared references (&T) only. Several threads hold it at once, so letting one of them change the value would be a data race:
use std::sync::Arc;
use std::thread;
fn main() {
let stock = Arc::new(vec![1, 2, 3]);
let for_worker = Arc::clone(&stock);
let handle = thread::spawn(move || {
for_worker.push(4);
});
handle.join().unwrap();
println!("{:?}", stock);
}
error[E0596]: cannot borrow data in an `Arc` as mutable
To change shared data, put a lock inside the Arc: Arc<Mutex<T>>, the next step.
Common mistakes
- Cloning the data instead of the pointer:
Arc::new(prices.clone())in the loop gives every thread its own copy. Clone theArc. - Cloning outside the loop: one clone moved into the first thread leaves nothing for the second:
E0382. Clone once per thread, inside the loop. - Using
Arcwhen borrowing would do: if all threads end inside one function,thread::scopelets them borrow, noArcneeded.
Your task
Three warehouse requests are checked against one shared stock table, each in its own spawned thread. Wrap stock in an Arc, give each thread its own clone, and have each thread return its line. Join in order and print:
2 x helmet: ok
5 x shield: short by 2
1 x sword: unknown item
A request is ok when the stock has at least the asked quantity, short by N when it has less, and unknown item when the item isn't in the table at all.
Challenge 2
Fill in the two blanks: put the menu behind an Arc, and give the worker its own pointer to it. The program should print the worker counted 3 dishes and then main still has 3 dishes.
Challenge 3
Write fn count_matches(lines: Arc<Vec<String>>, patterns: Vec<String>) -> Vec<usize>. Spawn one thread per pattern with thread::spawn (not a scope); each counts how many lines contain its pattern. Share the lines through the Arc (no copying of the lines) and return the counts in the same order as patterns.
Previous: Multiple producers: cloning the Sender Next: Mutex: one writer at a time