RustArena

Step 4 of 7

Scoped threads: borrowing local data

Copying chunks with to_vec() just to satisfy 'static is wasteful. Borrowing would be safe if you could promise that every thread ends before the borrowed data is dropped. thread::scope turns that promise into something the compiler can check:

use std::thread;

fn main() {
    let readings: Vec<u32> = (1..=12).collect();
    let total = thread::scope(|s| {
        let mut handles = Vec::new();
        for chunk in readings.chunks(4) {
            handles.push(s.spawn(|| chunk.iter().sum::<u32>()));
        }
        handles.into_iter().map(|h| h.join().unwrap()).sum::<u32>()
    });
    println!("total: {total}");
    println!("main still owns all {} readings", readings.len());
}
total: 78
main still owns all 12 readings

How it works:

No to_vec(), no move, no copies: each thread reads its chunk straight out of readings.

Mutable borrows work too

The borrowing rules still apply across threads: one &mut at a time per piece of data. chunks_mut cuts a slice into pieces that don't overlap, so each thread can get its own &mut piece:

use std::thread;

fn main() {
    let mut prices = vec![100, 250, 80, 40, 60, 500];
    thread::scope(|s| {
        for chunk in prices.chunks_mut(2) {
            s.spawn(|| {
                for p in chunk {
                    *p = *p * 90 / 100;
                }
            });
        }
    });
    println!("{prices:?}");
}
[90, 225, 72, 36, 54, 450]

This time we don't keep the handles: the scope joins all the threads for us before println! runs.

Lazy iterators and joining

A tempting one-liner is data.chunks(n).map(|c| s.spawn(...)).map(|h| h.join().unwrap()).sum(). Iterators are lazy (module 27): each item goes through both maps before the next chunk is touched, so every thread is joined right after it starts, and they run one at a time. Collect the handles into a Vec first, then join.

scope or spawn?

Common mistakes

Your task

Write fn parallel_sum(data: &[u64], workers: usize) -> u64. Split data into chunks of data.len().div_ceil(workers).max(1) items, sum each chunk in its own scoped thread, and return the total. Borrow the data; don't copy it. You can assume workers is at least 1.

Challenge 2

Write fn apply_discount(prices: &mut [u32], percent: u32, workers: usize). It lowers every price by percent percent, rounding down (price * (100 - percent) / 100), working on the slice in place with one scoped thread per chunk. Use chunks_mut with a chunk size of prices.len().div_ceil(workers).max(1).

Challenge 3

In one thread::scope, run two threads over the same borrowed temps vector at the same time: one finds the lowest reading, the other the highest. Then print them, and show that main still owns the vector:

min: -4
max: 31
readings: 7

Previous: Many threads, results in order Next: Send and Sync: what may cross threads