Question about using closures in multi threaded scenarios

Hi, im still learning Rust and Im really curious if I understand this part correctly.

use std::thread;

fn main() {
    let v = vec![1,2,3];

    let handle = thread::spawn(|| {
        println!("Here's a vector: {:?}", v);
    });

    handle.join().unwrap();
}

The reason the compiler infers a borrow here is because the closure decides what to use based on what the body of the function does with the captured values. Since the code in the closure only uses the value in a println! statement a borrow is the logical choice. The closure is not aware that it will be run in a new thread. Only later while compiling will this problem be captured by the compiler.

Is my thought process here correct, am I on the right track?

I mean “the closure is (not) aware [that xyz]” is giving a lot of personality to the closure; but nonwithstanding the way it’s expressed, you’re definitely on the right track.

In Rust, in several language features you might encounter an approach of leaving the control with the programmer, and only checking things, not fixing them for you.

Hence, if you’re implying to ask the question of “why does the closure create a borrow even though that can’t work for thread::spawn?” and suggesting the answer of “it’s because the knowledge that it’ll be passed to thread::spawn – which can’t use a closure with (non-'static) borrows – only comes later in compilation” is actually not quite accurate. This kind of knowledge actually arguably is present when the closure gets compiled. (For example, the closure body’s return type can also get type information from the spawn call.)

As far as I’m aware, it’s rather a deliberate design choice to keep the closure capturing rules relatively simple (they’re, already, not even really all that “simple” to begin with) and consistent, and locally determined, not to add too much additional smartness[1] just for the purpose of making more code compile, and instead to get the programmer involved again to fix their mistakes themselves, because often there could be multiple ways to “solve” an issue, or sometimes compilation bugs like this might even be indicative of something entirely different being wrong with a program.

In this particular case, you moreover have a borrow-checker error, and the borrow checker is particularly limited in that it’s never supposed to influence program behavior (instead it’s a true “checker”, and all that it can do is make compilation fail). As a consequence, you can understand how Rust programs work without needing to understand how the borrow checker works, and it’s easier for the compiler to ship updates/improvements to borrow checking without breakage.

It’s also now possible that API can be updated in ways that lifts/weakens lifetime restrictions (it’s not going to happen with thread::spawn in particular, but imagine a different library’s API with some F: 'static bound might be able to be generalized/improved in a way that removes this constraint in a later version) and we don’t want that to actually change (and possibly break) the behavior of any program using the API, either :slight_smile:


  1. perhaps an automatically added move or .clone() might seem appealing for a super-“smart” compiler ↩︎

1 Like