Why is thread::scope unable to copy primitive types without a move closure?

Viewed 55

Given this code:

use std::thread;

#[derive(Debug)]
struct ImportantStruct {}

fn main() {
    let mut data = vec![42, 42, 42, 42];
    let s = ImportantStruct{};
    
    thread::scope(|scope| {
        for (index, foo) in data.drain(..).enumerate() {
            scope.spawn(|| println!("thread {} doing {} while having a borrow on {:?}", index, foo, &s));
        }
    });
}

The rust compiler fails with the following error message:

error[E0373]: closure may outlive the current function, but it borrows `foo`, which is owned by the current function
  --> src/main.rs:12:25
   |
10 |     thread::scope(|scope| {
   |                    ----- has type `&'1 Scope<'1, '_>`
11 |         for (index, foo) in data.drain(..).enumerate() {
12 |             scope.spawn(|| println!("thread {} doing {} while having a borrow on {:?}", index, foo, &s));
   |                         ^^ may outlive borrowed value `foo`                                    --- `foo` is borrowed here
   |
note: function requires argument type to outlive `'1`
  --> src/main.rs:12:13
   |
12 |             scope.spawn(|| println!("thread {} doing {} while having a borrow on {:?}", index, foo, &s));
   |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
help: to force the closure to take ownership of `foo` (and any other referenced variables), use the `move` keyword
   |
12 |             scope.spawn(move || println!("thread {} doing {} while having a borrow on {:?}", index, foo, &s));
   |                         ++++

error[E0373]: closure may outlive the current function, but it borrows `index`, which is owned by the current function
  --> src/main.rs:12:25
   |
10 |     thread::scope(|scope| {
   |                    ----- has type `&'1 Scope<'1, '_>`
11 |         for (index, foo) in data.drain(..).enumerate() {
12 |             scope.spawn(|| println!("thread {} doing {} while having a borrow on {:?}", index, foo, &s));
   |                         ^^ may outlive borrowed value `index`                           ----- `index` is borrowed here
   |
note: function requires argument type to outlive `'1`
  --> src/main.rs:12:13
   |
12 |             scope.spawn(|| println!("thread {} doing {} while having a borrow on {:?}", index, foo, &s));
   |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
help: to force the closure to take ownership of `index` (and any other referenced variables), use the `move` keyword
   |
12 |             scope.spawn(move || println!("thread {} doing {} while having a borrow on {:?}", index, foo, &s));
   |                         ++++

For more information about this error, try `rustc --explain E0373`.
error: could not compile `playground` due to 2 previous errors

For some reason it is unable to copy the (copyable) index into the the scoped thread's scope, and I do not understand why.

Is something wrong with my code, or is this a rustc (rustc 1.63.0 (4b91a6ea7 2022-08-08)) bug? Should '1 in the error message have been replaced by something?

1 Answers

I think your main misconception is that you cannot use a move closure if you want to reference objects outside of the closure.

Yes, the closure moves/copies everything it uses into it. But you can create a reference to an object and then move the reference in. As non-mut references are implicitly Copy, it even lets you reuse said reference without taking ownership of it.

Like this:

use std::thread;

#[derive(Debug)]
struct ImportantStruct {}

fn main() {
    let mut data = vec![42, 42, 42, 42];
    let s = ImportantStruct {};

    thread::scope(|scope| {
        let s_ref = &s;
        for (index, foo) in data.drain(..).enumerate() {
            scope.spawn(move || {
                println!(
                    "thread {} doing {} while having a borrow on {:?}",
                    index, foo, s_ref
                )
            });
        }
    });
}
thread 0 doing 42 while having a borrow on ImportantStruct
thread 2 doing 42 while having a borrow on ImportantStruct
thread 1 doing 42 while having a borrow on ImportantStruct
thread 3 doing 42 while having a borrow on ImportantStruct

An alternative way of writing it is to wrap the move closure into another context where all the variables that will get moved in get instantiated.

It's personal preference. I personally think it is easier to read because it's clear that the variables are specifically only created for the closure.

use std::thread;

#[derive(Debug)]
struct ImportantStruct {}

fn main() {
    let mut data = vec![42, 42, 42, 42];
    let s = ImportantStruct {};

    thread::scope(|scope| {
        for (index, foo) in data.drain(..).enumerate() {
            scope.spawn({
                let s = &s;
                move || {
                    println!(
                        "thread {} doing {} while having a borrow on {:?}",
                        index, foo, s
                    )
                }
            });
        }
    });
}

It doesn't make much difference for FnOnces, but for FnMuts it makes things a lot cleaner.

For example, a fibonacci iterator could be written like this:

fn main() {
    let fib_iter = std::iter::from_fn({
        let mut i = 0;
        let mut j = 1;
        move || {
            let i_prev = i;
            let j_prev = j;
            i = j_prev;
            j = i_prev + j_prev;
            Some(i_prev)
        }
    });

    println!("{:?}", fib_iter.take(10).collect::<Vec<_>>())
}
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

Note that the variables i and j only exist inside of the closure, because they are created in the context that wraps it.

Related