Skip to content

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

Easy Primitives

move closures: why the compiler forces your hand

Split data into consecutive pieces, sum each piece on its own thread, and return the per-piece sums in order.

pub fn own_and_sum(data: Vec<i64>, parts: usize) -> Vec<i64>

The exact contract

  • If parts == 0 or data is empty, return an empty vector. Check this first: data.chunks(0) panics.
  • Otherwise compute size = data.len().div_ceil(parts), which is data.len() / parts rounded up, and split with data.chunks(size). Every chunk has size elements except possibly the last, which can be shorter.
  • This can produce fewer than parts chunks. For len = 5, parts = 4, size is 2, so the chunks have lengths [2, 2, 1]: three sums, not four. When parts is larger than data.len(), size is 1 and every element is its own chunk.
  • Return one sum per chunk, in chunk order (the order of data), regardless of which thread finishes first.
  • Values can be negative. The tests keep every chunk sum within the i64 range, so you do not need to handle overflow.

Example: data = [1, 2, ..., 10], parts = 3 gives size = 4, chunks [1..=4], [5..=8], [9, 10], and the result [10, 26, 19].

Why the starter does not compile

The starter is complete except for one keyword. Compiling it fails with error E0373:

error[E0373]: closure may outlive the current function, but it borrows
              `piece`, which is owned by the current function
...
help: to force the closure to take ownership of `piece` (and any other
      referenced variables), use the `move` keyword

Some terms first. A closure captures the variables it uses from its surroundings. By default it captures them in the least demanding way the body allows: piece.iter() only needs to read piece, so the closure captures a reference to piece, not piece itself. Here piece is the parameter of the outer .map(|piece| ...) closure, so it is destroyed when that call returns, which happens right after the thread is started.

std::thread::spawn requires its closure type to be Send + 'static. A 'static bound on a type means the type contains no references that could become invalid before the program ends; owned values such as Vec<i64> and references to 'static data satisfy it. The bound exists because the thread is not tied to the caller's stack frame: the returned JoinHandle can be dropped, which detaches the thread, and the thread can keep running after the function that started it has returned. A closure holding a reference to piece does not satisfy 'static, so the compiler rejects it. That is the meaning of "may outlive": the compiler does not claim the thread will run that long, only that the type allows it.

Writing move || changes how the closure captures: it takes every captured variable by value. The closure now owns the Vec<i64> named piece, contains no borrowed references, and satisfies 'static. (It is also Send, because Vec<i64> is Send.)

Moving means the caller gives the value up

After a value is moved into a closure, the original variable can no longer be used. For example, this fails with E0382 because numbers was moved into the thread:

let numbers = vec![1, 2, 3];
let handle = std::thread::spawn(move || numbers.iter().sum::<i64>());
println!("{}", numbers.len()); // error[E0382]: borrow of moved value: `numbers`

The starter avoids this: each piece is a separate vector created by .to_vec(), used only by its own thread, and never read again by the caller. The results come back as return values through join(), which waits for the thread and returns Ok with the closure's result (or Err if the thread panicked). If you hit E0382 while working with threads, a useful question is which value you need back from the thread; returning it through join() is often simpler than trying to keep using the moved variable.

.to_vec() copies each chunk into a new vector. That copy is one simple way to give each thread owned data; it is not the only one. Scoped threads (std::thread::scope) can borrow data directly without copying, because the scope waits for all of its threads before it returns.