Why does thread::spawn need static lifetime for generic bounds?

I have a function:

fn test<T: Send + Sync + std::fmt::Display>(val: T) {
    thread::spawn(move || println!("{}", val));
}

This fails to compile with error: the parameter type 'T' may not live long enough [E0310]. The compiler wants the additional 'static lifetime bound on T.

I'm just not sure why.

What about moving a generic type value with the proper Send + Sync bounds is not valid? Just sending static lifetime data isn't going to work for my purposes, I want to send copies/clones of other values. The value is moved to the closure and then the closure lifetime will be controlled by thread, why isn't that enough?

And why does the non-generic version work when neither the lifetime of val or String is static?

fn test2(val: String) {
    thread::spawn(move || println!("{}", val));
}

What would be the proper way to send generic types to another thread? Will I need to use channels?

8 Likes

The 'static bound on a type doesn't control how long that object lives; it controls the allowable lifetime of references that object holds.

Without such a bound, since T is generic and only bounded by the traits given, val may have references to objects of any arbitrary lifetime. For instance, you might write this function:

fn evil() {
    let x = 10;
    test(&x);
}

Now val is a reference to a variable on the stack. But the thread that you've spawned could keep running after test and then evil have returned. Now that pointer is dangling, pointing to some memory that has been reused for something else.

The reason that it works with String is that String does comply with the 'static bound; String is a concrete type, and doesn't contain any borrowed references. A generic type T, however, could contain references of any possible lifetime, and the thread could outlive any of those lifetimes.

I think one of the key things that a lot of people trip over is thinking that lifetime annotations refer to the lifetime of the object they are applied to. They do not; they refer to the minimum possible lifetime of any borrowed references that the object contains. This, of course, constrains the possible lifetimes of that object; an object cannot outlive its borrowed references, so its maximum possible lifetime must be shorter than the minimum possible lifetimes of the references it contains.

When handing an object off to a thread, it must have only 'static references, because the new thread could outlive the original thread.

Now, there was once a way to do what you want; spawn a thread that captures a value that has references of lifetimes shorter than 'static. It was called std::thread::scoped. It worked by returning a JoinGuard when invoked, which would block the invoking thread on Drop (when the JoinGuard went out of scope, at the end of the block that spawned the thread) until the spawned thread exited, to ensure that all stack variables would still be live for the full duration of the thread. Sadly, this assumption turned out to have a fatal flaw, as it was possible to leak the JoinGuard, causing Drop to never be called. This caused much wailing and gnashing of teeth, but eventually meant that thread::scoped had to be dropped as it was unsound (could lead to undefined behavior).

Luckily, there is a way around this, implemented in the crossbeam crate. You can guarantee that your guard won't leak, by passing it in to a closure as a borrowed reference; the function that passes it in, crossbeam::scope, then owns the scope and can prevent it from leaking, allowing passing borrowed references to be passed in to new threads safely, as the crossbeam::scope method can block and not return until all of the threads spawned from its scope have exited, and there's no way to leak that.

Anyhow, for your purposes, you may or may not need borrowed references to be able to be passed in. If you don't, just add a 'static bound to your type parameter, and you'll be all set, and should be able to pass in objects that own their contents like String, Vec<T>, Box<T> (the latter two only if T itself is also bounded by 'static), integers, structs containing the preceding types, string literals (which are references, but they are 'static references that live as long as the whole program) and so on. If you do need to pass in objects that could contain borrowed references of lifetimes shorter than 'static, you'll need to use something like crossbeam::scope.

52 Likes

Would it make sense to change the parameter type 'T' may not live long enough to the type 'T' may contain references that don't live long enough then?

4 Likes

By the way, just to demonstrate some of the things you can use that meet the 'static bound:

use std::thread;
use std::fmt;
use std::time::Duration;

fn test<T: Send + Sync + std::fmt::Display + 'static>(val: T) {
    thread::spawn(move || println!("{}", val));
}

fn main() {
    test("hello");                // &'static str
    test(String::from("hello"));  // String
    test(5);                      // i32
    struct Foo {
        string: String,
        v: Vec<f64>,
    };
    impl fmt::Display for Foo {
        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
            write!(f, "{}: {:?}", self.string, self.v)
        }
    }
    // Arbitrary struct containing String and Vec<f64>
    test(Foo {string: String::from("hi"), v: vec![1.2, 2.3]});
    thread::sleep(Duration::new(1, 0));
}
3 Likes

Doesn't have to contain references at all.

// Does not *actually* contain any references:
struct LifetimeLimited<'a>(std::marker::PhantomData<&'a ()>);

Even without any, LifetimeLimited is still limited in how long it's allowed to exist.

Aha, that was the missing piece of my puzzle! This is definitely something that is very unclear in the error message The rustc --explain example for error was an explanation with a concrete type, which further led to my confusion when it came to generics.

My thinking was that that an instance of struct Foo<'static> { ... } must always exist in a static context, rather than simply constraining references. This makes a lot more sense how type bounds work.

And yes, I didn't really need scoped threads, I just wanted to fire off a thread with and forget about it. Now I can get back to doing more than just printing a value in the thread :smirk:

6 Likes

if I pass a String into a FnOnce, then run it in a thread. When the thread ends, will the String be released, though 'static?

  1. Please do not revive old threads - create a new one and link to the old for context, if necessary.
  2. T: 'static doesn't mean "this will live forever" - only "this is allowed to life forever, you're not forced to drop it". See also this list.
1 Like

This topic was automatically closed 30 days after the last reply. We invite you to open a new topic if you have further questions or comments.