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 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 chunking convention is fixed, so read it carefully. Use data.chunks(size) with size = data.len().div_ceil(parts). That is the same rule the standard library uses: equal-sized pieces with a possibly-short tail. It may produce fewer than parts pieces — for len = 5, parts = 4 the size is 2, so you get three chunks [2, 2, 1], not four. Return one sum per chunk, in chunk order.

Guard the degenerate inputs: parts == 0 and an empty data both return []. (chunks(0) panics, so you must handle them before you get there.)

The starter does not compile, and that is the lesson

It fails with E0373:

error[E0373]: closure may outlive the current function, but it borrows
              `piece`, which is owned by the current function
   |
   |     .map(|piece| thread::spawn(|| piece.iter().sum::<i64>()))
   |                                 ^^ ----- `piece` is borrowed here
   |                                 |
   |                                 may outlive borrowed value `piece`
help: to force the closure to take ownership of `piece`, use the `move` keyword

Read the first clause literally: may outlive. The compiler is not claiming your thread will still be running when piece dies. It is saying it cannot prove otherwise, and for thread::spawn there is nothing to prove — the handle can be dropped, the thread detached, and the closure is then free to run for the rest of the process’s life.

That is why thread::spawn requires F: Send + 'static. The 'static bound is the formal way of saying “this closure must not borrow anything with a shorter life than the program”. A closure that borrows a local is not 'static, so it is rejected — before any thread exists, at compile time, every time. This is the whole “fearless” claim in one bound.

move fixes it by changing what the closure captures: instead of a reference to piece, it captures piece itself. The Vec is moved into the closure, the closure owns it outright, and its lifetime is now 'static because it borrows nothing.

The second error, which is also part of the lesson

Adding move usually produces a new error, so expect it:

error[E0382]: borrow of moved value: `data`

If you move something into a thread and then read it afterwards in the parent, the parent no longer owns it. There is exactly one value, and you gave it away. This trips people because “it’s just a thread” feels like it should not affect ownership — but a thread is not a special case, it is an ordinary closure that happens to run elsewhere.

The fix here is structural rather than syntactic: do not read the moved data again — return the results instead. Each worker owns its piece, computes from it, and hands back a number through join(). Nothing needs to look at data afterwards. When you find yourself fighting E0382 around a thread, the question is almost always “what value do I actually need out of this?” rather than “how do I get my variable back?”.

This is also why the signature takes Vec<i64> by value and the chunks get .to_vec()‘d. Copying the data is the crude way to satisfy 'static. Two problems from now thread::scope will let you delete both the copy and the move, and you will appreciate it far more for having done it this way first.

Loading visualization…