Skip to content

← Fearless Concurrency: Threads, Channels, Shared State step 4 of 24

Easy Primitives

Scoped threads: borrow, do not clone

Split data into contiguous chunks and return the maximum of each, in chunk order — with no copying of the data.

pub fn chunk_maxes(data: Vec<i64>, workers: usize) -> Vec<i64>

Rules for this one:

  • everything happens inside a single std::thread::scope;
  • each worker thread borrows its &[i64] — no .to_vec(), no Arc, no cloning of any kind;
  • chunk size is data.len().div_ceil(workers), matching slice::chunks, so you may end up with fewer than workers chunks;
  • workers == 0 and empty data both return []. Guard them: chunks(0) panics and so does div_ceil(0).

Why the previous problem had to copy

thread::spawn demands F: Send + 'static, and 'static means the closure borrows nothing that could die first. The compiler has no idea when a spawned thread ends — the handle can be dropped and the thread detached — so it must assume the worst.

thread::scope (stable since 1.63) changes the premise. The scope joins every thread it spawned before it returns, and that is enforced by the API, not by convention. Because the compiler now knows every thread ends before the scope does, the 'static bound is gone. Threads inside a scope may borrow local data:

let data = vec![1, 2, 3];
thread::scope(|s| {
    s.spawn(|| println!("{:?}", &data));   // borrowing a local. fine.
});
// every scoped thread has been joined by here

You still get handles back from s.spawn, and you still join them yourself when you want a value. The automatic join at the end of the scope is a guarantee about lifetimes, not a way to collect results in order.

This is the correct default for the shape of work in this track — read-only input, split into disjoint pieces. Reach for Arc when you genuinely need shared ownership, not as ceremony to appease 'static.

The clippy lesson, and it is the point of the starter

The starter computes the chunk size the way every C programmer’s fingers do:

let size = (data.len() + workers - 1) / workers;

It compiles. It is arithmetically correct for the inputs here. And it fails the gate, because clippy::manual_div_ceil has been warn-by-default since 1.83:

warning: manually reimplementing `div_ceil`
  |
  |     let size = (data.len() + workers - 1) / workers;
  |                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |                help: consider using `.div_ceil()`: `data.len().div_ceil(workers)`

Take this one seriously rather than treating it as style. len + workers - 1 can overflow for large len, and overflow checks are off in this build, so it would wrap silently and hand you a chunk size near zero. div_ceil cannot overflow. The lint is protecting you from a real bug that would only show up on large inputs, and clippy caught it without running your code.

The other error worth meeting

If you try to accumulate into a shared local from two scoped threads:

let mut count = 0;
thread::scope(|s| {
    s.spawn(|| count += 1);
    s.spawn(|| count += 1);   // error[E0499]: cannot borrow `count` as
});                           //   mutable more than once at a time

thread::scope relaxed 'static. It did not relax aliasing-xor-mutation — two &mut to the same local is still two &mut to the same local, whether or not threads are involved. That is the borrow checker preventing a data race with a rule it already had, which is a fair summary of why “fearless concurrency” is not a separate feature bolted on.

Loading visualization…