Scoped thread: why the code can't be compiled

fn main() {
    let s1 = String::from("11");
    let sr = &s1;

    // F: for<'scope> FnOnce(&'scope Scope<'scope, 'env>) -> T,
    let _x = std::thread::scope(|s| {
        // pub fn spawn<F, T>(&'scope self, f: F) -> ScopedJoinHandle<'scope, T>
        // where
        //    F: FnOnce() -> T + Send + 'scope,
        //    T: Send + 'scope,
        let xx = s.spawn(|| sr);

        // ScopedJoinHandle<'scope, &String>
        xx
    });
}

was refused by

error: lifetime may not live long enough
  --> src/main.rs:13:9
   |
7  |     let _x = std::thread::scope(|s| {
   |                                  -- return type of closure is ScopedJoinHandle<'2, &String>
   |                                  |
   |                                  has type `&'1 Scope<'1, '_>`
...
13 |         xx
   |         ^^ returning this value requires that `'1` must outlive `'2`

but xx is ScopedJoinHandle<'scope, &String>, and it should be true that 'scope : 'scope

As @steffahn pointed out in a great answer to one of your previous questions concerning thread::scope, this is due to the use of the HRTB on 'scope, which makes it impossible to leak the ScopedJoinHandle from the closure:

(not sure if you already read tracking issue for scoped threads, but if not I'm sure you'll find it quite interesting, various design considerations that were taken into account when developing the API are discussed).

2 Likes

I don't know why HRBT here can forbid ScopedJoinHandle<'scope, &String> moving into main.

Doesn't HRBT logically do the following?

for 'any in all_lifetimes_from_zero_to_unlimited {
    if not 'env: 'any {
        continue;
    }

    compile {
         let _x = std::thread::scope(|Scope<'scope, 'env>| {
             let xx = s.spawn(|| sr);
             xx
         });
   }
}

If that is what it does, then when compile the code, 'scope holds a specific lifetime 'any, which satisfies 'env: 'any.

considering the 'any, seems there is nothing that can stop the code from being compiled. The signature of ScopedJoinHandle<'scope, &String> means it can lives until 'scope reaching its end. and the returning T does not hold any constrains

At a conceptual level, 'scope ends somewhere within the call to thread::scope(...). Because this returns the T returned by the closure, it is logically impossible for that T to refer to 'scope— If it did, the return value of thread::scope would be a dangling reference.

I'm not good enough at following the technical rules to describe how they interact to guarantee that outcome, unfortunately.

3 Likes

It's kinda like that, but the call to std::thread::scope is outside the for, which is crucial here, because it means that its parameters ('env, T and F) must be chosen outside the for. Your T however tries to use the 'any lifetime from inside the for, which is not possible just like it's not possible to use the variable of a for loop before the loop starts.


Edit: to be more explicit:

Let's say the compiler is trying to check some std::thread::scope(f). It will first have to determine its generic parameters 'env, T and F. In your case 'env = '1, T = ScopedJoinHandle<'2, &'3 String> and F = {some closure}. Most notably, '2 cannot be 'scope, because 'scope doesn't exist at this point.

If you then expand the logical meaning of the HRTB, which you can imagine it as a kind of loop:

for 'scope in all_lifetimes_from_zero_to_unlimited {
    // This reduces to `if ('env: 'scope)`
    if (FnOnce(&'scope Scope<'scope, 'env>) -> T) is well formed {
        check that F: FnOnce(&'scope Scope<'scope, 'env>) -> T
    }
}

You can then see that in your case F does implement FnOnce(&'scope Scope<'scope, 'env>) -> ScopedJoinHandle<'scope, &'3 String>, and to satisfy the previous condition this ends up requiring ScopedJoinHandle<'scope, &'3 String> to be a subtype of T. This in turn requires 'scope: '2, however given any fixed '2previously chosen I can pick a smaller 'scope for which this bound won't hold, and thus the condition is false and your code is not accepted.

5 Likes

I still can't understand.

You can't pick a smaller 'scope because the bound defined at Scope::spawn

        // pub fn spawn<F, T>(&'scope self, f: F) -> ScopedJoinHandle<'scope, T>
        // where
        //    F: FnOnce() -> T + Send + 'scope,
        //    T: Send + 'scope,

I don't know whether is right: to make std::thread::scope getting compiled, the only precondition is the lambda (let's name it L)

    |s| {
        // pub fn spawn<F, T>(&'scope self, f: F) -> ScopedJoinHandle<'scope, T>
        // where
        //    F: FnOnce() -> T + Send + 'scope,
        //    T: Send + 'scope,
        let xx = s.spawn(|| sr);

        // ScopedJoinHandle<'scope, &String>
        xx
    }

is a subtype of for<'scope> FnOnce(&'scope Scope<'scope, 'env>) -> T, e.g. L : `for<'scope> FnOnce(&'scope Scope<'scope, 'env>) -> T.

because L captured sr, then L should be something like that:

struct L<'env> {
    sr: &'env String
}

impl<'scope, 'env: 'scope> FnOnce<(&'scope Scope<'scope, 'env>, )> for L<'env> {
    type Output = ScopedJoinHandle<'scope, &'scope String>;
    extern "rust-call"  fn call_once(self, args: (&'scope Scope<'scope, 'env>,)) -> Self::Output {
        let sr = self.sr as &'scope String;
        let xx  = args.spawn(|| sr);
        xx
    }
}

So, it is true that L<'env> : for<'scope> FnOnce(&'scope Scope<'scope, 'env>) -> T

No, what I mean is that the for 'scope in all_lifetimes_from_zero_to_unlimited { will eventually each a lifetime that is not bigger than '2.

In fact std::thread::scope is allowed to (and actually does) give you a &'scope Scope<'scope, 'env> where 'scope is smaller than any lifetime nameable from the outside of std::thread::scope, and thus smaller than '2. That lifetime is the lifetime of a local variable inside the stack of std::thread::scope, which will of course not be valid anymore the moment it returns to its caller.

This is kinda what your closure type looks like, yes. However it does not satisfy the requirements of std::thread::scope, which look like this:

struct L<'env> {
    sr: &'env String
}

// T must be fixed before considering the context inside the `for<'scope>`.
// At that point 'env is defined so it can refer to it, however it cannot refer to 'scope
// because it doesn't exist yet. So what do you put into that lifetime '?? then?
type T<'env> = ScopedJoinHandle<'??, &'env String>

impl<'a, 'env: 'a> FnOnce<(&'a Scope<'a, 'env>, )> for L<'env> {
    type Output = T;
    extern "rust-call"  fn call_once(self, args: (&'a Scope<'a, 'env>,)) -> Self::Output {
        let xx  = args.spawn(|| self.sr);
        // Error: this is a `ScopedJoinHandle<'scope, &'env String>`,
        // but a `ScopedJoinHandle<'??, &'env String>` was expected
        // and no matter what '?? is, it is unrelated to 'scope
        // so the given `ScopedJoinHandle<'scope, &'env String>` cannot be
        // converted into the expected `ScopedJoinHandle<'??, &'env String>`.
        xx
    }
}
2 Likes

It can't

fn main() {
    let s1 = String::from("11");
    let sr = &s1;

     std::thread::scope(|s: Scope<'scope, 'env>| {
        let n = 9;  // '1
        s.spawn(|| n); // F: 'scope
    });
}

because 'scope : '1 and F: 'scope, F can't capture '1

You are confusing std::thread::scope with s.spawn

Your original question can be answered without appealing to logic about scoped threads at all.

pub fn scope<'env, F, T>(f: F) -> T
where
    F: for<'scope> FnOnce(&'scope Scope<'scope, 'env>) -> T,

The bound on F is the same sort of bound that comes up on this forum a few times a month, which you should be familiar with after our other discussions: Why can't this compile?

fn takes_nonborrowing_fn<F: for<'any> Fn(&'any str) -> R, R>(_: F) {}

fn main() {
    takes_nonborrowing_fn(str::trim);
}

It's because R represents a single type, so R can't rely 'any -- that would be a different type per input lifetime 'any.

Likewise, T can't rely on 'scope, so you can't return a ScopedJoinHandle<'scope, _> from the closure you pass to scope.

4 Likes