4

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?

djfm
  • 2,317
  • 1
  • 18
  • 34
  • 1
    Does this answer your question? [How can I pass a reference to a stack variable to a thread?](https://stackoverflow.com/questions/32750829/how-can-i-pass-a-reference-to-a-stack-variable-to-a-thread) – Herohtar May 02 '22 at 18:35

2 Answers2

3

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.

cdhowie
  • 158,093
  • 24
  • 286
  • 300
  • thanks, I'll give crossbeam a try! I think the key concept I was missing is that the borrow checker doesn't understand that joining the thread ends the borrow, shouldn't it? Or am I missing something? – djfm May 02 '22 at 18:49
  • @djfm It would have to be an explicit special case in the borrow checker. There's no syntax in Rust for "calling this method ends some other borrow but _not_ calling this method _won't_ end the borrow." – cdhowie May 02 '22 at 18:53
  • @djfm Note that you can get this to work by using an `unsafe` block, where you can cast a reference to have a different lifetime. This is how crossbeam works under the hood -- it casts the lifetime of closures to `'static` even when they capture local variables, because crossbeam guarantees that the thread ends before returning control to the outer function, where the owned value could be destroyed. In other words, it uses `unsafe` under the hood but exposes a _safe_ interface (which is what a lot of Rust itself does). – cdhowie May 02 '22 at 18:58
  • @djfm So you could do this yourself, but then _you_ are responsible for upholding the memory safety guarantees. It would be a better choice, IMO, to lean on crossbeam to do this checking for you. – cdhowie May 02 '22 at 19:00
0

Here is the way I solved this issue.
I used Box::Leak: https://doc.rust-lang.org/std/boxed/struct.Box.html#method.leak

let boxed_data = data.into_boxed_slice();
let boxed_data_static_ref = Box::leak(boxed_data);

let compressed_data = &boxed_data_static_ref[start_of_data..start_of_data+zfr.compressed_size as usize];


let handles = (0..NUM_THREADS).map(|thread_nr| {
    thread::spawn(move || {
        main2(thread_nr, compressed_data, zfr.crc);
    })
}).collect::<Vec<_>>();

for h in handles {
    h.join().unwrap();
}
Ray Hulha
  • 10,701
  • 5
  • 53
  • 53