We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Fearless Concurrency: Threads, Channels, Shared State step 4 of 24
Scoped threads: borrow, do not clone
Divide a vector into contiguous chunks and compute each chunk's maximum in a scoped thread. Return the maxima in chunk order.
pub fn chunk_maxes(data: Vec<i64>, workers: usize) -> Vec<i64>
Return an empty vector when workers == 0 or data is empty. Otherwise:
- Set the chunk size to
data.len().div_ceil(workers). - Use
data.chunks(size)to get consecutive, nonempty slices. - Inside one
std::thread::scope, spawn a worker for each chunk. - Have each worker borrow its slice and return its maximum.
- Collect the results in the same order as the input chunks.
For [3, 1, 4, 1, 5, 9, 2, 6] with three workers, the chunk size is three:
| Chunk | Maximum |
|---|---|
[3, 1, 4] |
4 |
[1, 5, 9] |
9 |
[2, 6] |
6 |
The output is [4, 9, 6], regardless of which thread finishes first.
workers is an upper bound on the number of chunks, not a requirement to
create that many threads. For two values and five workers, the chunk size
is one and only two workers are needed.
Borrow the input storage rather than cloning chunks into new vectors or
wrapping the input in Arc. Copying an i64 while finding the maximum,
or copying a slice reference into a closure, is allowed. The restriction
concerns duplicating the input buffer, not every use of the Copy trait.
Output tests alone do not enforce the threading or borrowing strategy.
Why a scope allows local borrows
thread::spawn requires its closure and returned value to satisfy 'static
as well as Send. The lifetime bound prevents them from depending on a
borrow that could expire while the thread is still running. It does not
mean owned values must live forever: a closure can own a Vec and drop it
when it finishes. Moving ownership is one alternative to borrowing; copying
is not always required.
Scoped threads have a tighter lifetime. Threads spawned through the scope
are joined before thread::scope returns, so they may borrow data that
remains valid for that scope:
use std::thread;
let data = vec![1, 2, 3];
thread::scope(|s| {
s.spawn(|| assert_eq!(data.len(), 3));
});
assert_eq!(data, [1, 2, 3]);
The thread's closure must still be Send. Sharing &[i64] works because
i64 supports shared access across threads. A scope does not make every
type safe to share or remove the borrowing rules.
With s.spawn(move || ...), the closure takes its slice reference by value.
It does not take ownership of the vector or clone its elements. The vector
stays alive outside the scope while all workers read their slices.
Preserve chunk order when joining
First collect the handles into a vector in chunk order. Then join those handles in that order and collect their returned maxima. This starts all workers before waiting for their results. Spawning and immediately joining each worker would serialize the work instead.
Automatic joining ensures threads cannot outlive the scope, but it does not
assemble their return values for you. Explicit join returns a result so
you can retrieve each maximum and handle a worker panic. If a thread left
for automatic joining panics, thread::scope itself panics.
Each chunk is nonempty, so chunk.iter().copied().max() returns Some.
An expect can document that invariant. Negative values are valid; do not
start a maximum at zero and accidentally reject an all-negative chunk.
Avoid overflow in the chunk-size formula
The starter computes:
let size = (data.len() + workers - 1) / workers;
The numerator can overflow. This can happen with a small vector and a very
large workers value, not just a large allocation. Depending on overflow
settings, the arithmetic can panic or wrap and produce an incorrect size.
Use data.len().div_ceil(workers). For these unsigned values, it computes
the ceiling of the division without overflowing that intermediate sum.
Still handle zero workers before dividing, and empty input before calling
chunks: division by zero and chunks(0) panic.
Clippy's manual_div_ceil lint points out this pattern. Fix the calculation
rather than suppressing the warning. Passing also requires compilation,
correct outputs and Clippy with warnings treated as errors.
A scope does not allow overlapping mutable borrows
This example is rejected because both closures would borrow the same local mutably for overlapping lifetimes:
use std::thread;
let mut count = 0;
thread::scope(|s| {
s.spawn(|| count += 1);
s.spawn(|| count += 1);
});
For shared updates, choose appropriate synchronization or split the data into disjoint mutable regions. This exercise only reads shared slices and returns separate results, so no shared mutable accumulator is needed.
Scoped threads are useful when borrowed work must finish before returning. They are not a promise of better performance: spawning operating-system threads can cost more than scanning small chunks. Measure before choosing this strategy for a production workload.
Your code, saved here
Sign in to access your accepted solutions and saved draft history. You can keep reading the lesson without an account.
Sign in to your accountStuck?
Rust reference solution
Sign in to track your attempts toward unlocking the reference solution.