E0373 when underlying vector does outlive thread borrowing it

I'm new to Rust and trying to compile some lecture notes to help some students learn about the subject and I came across this in chapter 16 of the book: https://doc.rust-lang.org/book/ch16-01-threads.html . I'm running the example about spawning a thread with the vector where a closure takes place with a slight modification.

Here's the code I'm working with:

use std::thread;

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

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

    handle.join().unwrap();
    println!("{:?}", v);
}

I've added the last line println!("{:?}", v); to the example. Without it, it is clear that v may not live long enough, because the closure may outlive the current function. However, I expected that by adding a usage of v after the join(), that it would mean the compiler can reason that the vector v must live past the join() and therefore outlive the thread and its closure. Still, the compiler says error[E0373]: closure may outlive the current function, but it borrows v, which is owned by the current function

Maybe that's incorrect, though? I thought about if I should open this as an issue in Github, but seeing that this forum exists I thought maybe I'd ask here first in case I'm overlooking something.

I'm working with Rust in macOS using 1.39.0 + CLion environment.

Here's the signature of thread::spawn:

pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
    F: FnOnce() -> T,
    F: Send + 'static,
    T: Send + 'static,

The F: 'static bound means that the closure passed can't capture non-'static data from its environment. The placement of join() doesn't have any effect, and neither does the later use of v. As far as the compiler is concerned, what's wrong here is that you tried to pass a non-'static closure to a function that requires a 'static closure, and that's that.

The beautiful thing about this is that it's plain and simple Rust. The compiler doesn't know what threads are, or have to care about what spawn does internally. It can check the constraints using only local information.

The drawback is that it cannot safely express the entire scope of what you can do with threads. Even if it did work the way you expected in this trivial case, it's easy to construct a scenario that is actually safe, but in which the compiler can't statically determine that join is always called -- and you'd be back to this same problem.

There are crates that provide scoped threads, which expose a safe API for doing stuff like this. It's a little more complex than std::thread, but not extremely so. Here's an example using crossbeam.

The other good option is to make the captured data 'static so that even if the child thread isn't joined, it still can't access an invalidated reference. One way to do that, besides just leaking the data, is reference counting: put the data in an Arc so that it won't be destroyed until all the threads that still hold a reference to it are all done. Here's what that might look like.

1 Like

Another option here is to let the method signatures show the compiler that the vector is moved back out of the closure, by returning it and saving it from .join():

fn main() {
    let v = vec![1, 2, 3];
    let t = thread::spawn(|| {
        // do something with v
        v
    });
    let v = t.join().unwrap();
     // do something else with v
}
2 Likes

The short explanation is that the compiler is not able to make this inference.

For your code to compile, it is not enough that it is memory safe. You also have to convince the compiler that this is the case, and in your case, you are missing the "convince the compiler" part.

Also note that joining a thread doesn’t guarantee that all the variables captured in that thread’s closure have been destroyed. For example, here’s a slightly modified version of your example that

  • Uses a move closure to allow the code to compile, and
  • Spawns a tertiary thread inside the seconday one:
use std::thread;

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

    let handle = thread::spawn(move || {
        thread::spawn(move || {
            thread::sleep(std::time::Duration::from_millis(100));
            println!("Here's a vector: {:?}", v2);
        });
    });

    handle.join().unwrap();
    println!("{:?}", v);

    // sleep to let other thread finish
    thread::sleep(std::time::Duration::from_millis(300));
}

Output:

[1, 2, 3]
Here's a vector: [1, 2, 3]

(Playground)

2 Likes