Step 3 of 7
Many threads, results in order
Real work gets split over several threads. The pattern every team uses:
- Spawn the threads in a loop and keep every
JoinHandlein aVec. - When all of them are running, join them in the order you spawned them.
Each thread finishes whenever it finishes, but because you collect results in spawn order, the output is the same on every run. This example makes it visible with thread::sleep, which pauses the current thread for a Duration (from std::time). The workers that sleep longer finish later:
use std::thread;
use std::time::Duration;
fn main() {
let jobs = vec![("resize", 30), ("compress", 10), ("upload", 20)];
let mut handles = Vec::new();
for (name, ms) in jobs {
handles.push(thread::spawn(move || {
thread::sleep(Duration::from_millis(ms));
format!("{name} done after {ms} ms")
}));
}
for handle in handles {
println!("{}", handle.join().unwrap());
}
}
resize done after 30 ms
compress done after 10 ms
upload done after 20 ms
compress finished first, but main waited for resize first, so the results come out in spawn order. Joining a thread that already finished returns at once. The whole program takes about 30 ms, not 60, because the three sleeps overlap.
move is needed here: each closure uses name and ms, which are local to one turn of the loop.
Splitting data into chunks
To process a big list, give each thread one piece. chunks(n) (module 27) cuts a slice into pieces of n items (the last one may be shorter). A spawned thread must own its data, so each borrowed chunk is copied into its own Vec with to_vec():
use std::thread;
fn main() {
let readings: Vec<u32> = (1..=12).collect();
let mut handles = Vec::new();
for chunk in readings.chunks(4) {
let part = chunk.to_vec();
handles.push(thread::spawn(move || part.iter().sum::<u32>()));
}
let sums: Vec<u32> = handles.into_iter().map(|h| h.join().unwrap()).collect();
println!("partial sums: {sums:?}");
println!("total: {}", sums.iter().sum::<u32>());
}
partial sums: [10, 26, 42]
total: 78
Copying every chunk costs time and memory. The next step removes the copy.
How many threads?
Threads aren't free: each one gets its own stack (2 MiB by default for spawned threads) and takes some microseconds to start. For CPU-heavy work, about one thread per core is right; thread::available_parallelism() tells you how many cores the machine offers. Spawning a thread per item for a million items would spend more time managing threads than working.
Common mistakes
- Joining inside the spawn loop:
handles.push(thread::spawn(f).join().unwrap())waits for each thread before starting the next, so they run one at a time. - Looping over
&handles:jointakes the handle by value (it consumes it), sofor h in &handles { h.join(); }fails witherror[E0507]: cannot move out of `*h` which is behind a shared reference. Loop overhandlesitself. - Expecting the fastest worker's result first: results come in the order you join, which is the point.
Your task
Spawn 4 threads. Thread i (for i from 0 to 3) adds up the squares of the numbers from i * 25 + 1 to (i + 1) * 25, inclusive. After all 4 are running, join them in order, print each result, then the total:
worker 0: 5525
worker 1: 37400
worker 2: 100525
worker 3: 194900
total: 338350
Challenge 2
Count the prime numbers below 1000 with 4 threads. Thread i (0 to 3) checks the numbers from i * 250 up to, but not including, (i + 1) * 250, using the is_prime function you're given. Join in order and print:
worker 0: 53 primes
worker 1: 42 primes
worker 2: 37 primes
worker 3: 36 primes
total: 168 primes
Challenge 3
Write fn totals_in_threads(orders: Vec<Vec<u32>>) -> Vec<u32>. Each inner Vec holds the line prices of one order. Spawn one thread per order to add up its prices, and return the totals in the same order as the input.
Previous: move closures: giving data to a thread Next: Scoped threads: borrowing local data