Home > Software engineering >  Borrowed value does not live long enough when used by thread
Borrowed value does not live long enough when used by thread

Time:05-03

So I'm pursuing my Rust adventures (loving it) and I'm exploring threads. As usual I stumbled upon an error that I do not understand.

Here is a minimal example:

use std::thread;

pub fn compute_something(input: &Vec<&usize>) -> usize {
    input.iter().map(|v| *v).sum()
}

pub fn main() {
    let items = vec![0, 1, 2, 3, 4, 5];
    
    let mut slice: Vec<&usize> = Vec::new();

    slice.push(&items[1]); // borrowed value does not live long enough
    // argument requires that `items` is borrowed for `'static`
    slice.push(&items[2]); // borrowed value does not live long enough
    // argument requires that `items` is borrowed for `'static`

    assert_eq!(3, compute_something(&slice));
    
    let h = thread::spawn(move || compute_something(&slice));
    
    match h.join() {
        Ok(result) => println!("Result: {:?}", result),
        Err(e) => println!("Nope: {:?}", e)
    }
} // `items` dropped here while still borrowed

I have of course made a playground to illustrate.

If I drop the thread part (everything after the assert_eq! line) and just call compute_something(&slice) it compiles fine.

There are three main things I don't understand here:

  • Why is it a problem to drop items while borrowed at the end of the program, shouldn't the runtime clean-up the memory just fine? It's not like I'm gonna be able to access slice outside of main.

  • What is still borrowing items at the end of the program? slice? If so, why does that same program compile by just removing everything after the assert_eq! line? I can't see how it changes the borrowing pattern.

  • Why is calling compute_something from inside the thread's closure creating the issue and how do I solve it?

CodePudding user response:

You move slice into the closure that you pass to thread::spawn(). Since the closure passed to thread::spawn() must be 'static, this implies that the vector being moved into the closure must not borrow anything that isn't 'static either. The compiler therefore deduces the type of slice to be Vec<&'static usize>.

But it does borrow something that's not 'static -- the values that you try to push into it borrow from a variable local to main(), and so the compiler complains about this.

The simplest way to fix this case is to have slice be a Vec<usize> and not borrow from items at all.

Another option is to use scoped threads from the crossbeam crate, which know how to borrow from local variables safely by enforcing that all threads are joined before a scope ends.


To directly answer the questions you posed:

Why is it a problem to drop items while borrowed at the end of the program, shouldn't the runtime clean-up the memory just fine?

When main() terminates, all threads are also terminated -- however, there is a brief window of time during which the values local to main() have been destroyed but before the threads are terminated. There can exist dangling references during this window, and that violates Rust's memory safety model. This is why thread::spawn() requires a 'static closure.

Even though you join the thread yourself, the borrow checker doesn't know that joining the thread ends the borrow. (This is the problem that crossbeam's scoped threads solve.)

What is still borrowing items at the end of the program?

The vector that was moved into the closure is still borrowing items.

Why is calling compute_something from inside the thread's closure creating the issue and how do I solve it?

Calling this function isn't creating the issue. Moving slice into the closure is creating the issue.

  • Related