Why does `tokio::spawn` requires a `'static` lifetime if I immediately await it?
Asked Answered
C

1

6

I want to run two Future in parallel, and if possible in different threads :

try_join!(
  tokio::spawn(fut1), // fut1 is not 'static
  tokio::spawn(fut2)
)?;

If my understanding is correct, tokio::spawn requires a Future to be 'static because the execution is started immediately and it has no guarantee that the future will not outlive current scope.

However in my case I immediately await them, so I know it won't outlive the current scope.

Is my reasoning correct ? If not, what is unsafe in passing non 'static arguments in my case ?

Coleen answered 20/12, 2022 at 15:38 Comment(1)
There's no way that tokio::spawn is implemented differently depending on whether the futures are immediately awaited or not. It is defined to require Future + Send + 'static for its argument.Macrospore
O
7

However in my case I immediately await them, so I know it won't outlive the current scope.

There are two responses to this line of reasoning.

One is that the fact that you're immediately awaiting simply has no bearing on the checks performed by the compiler. tokio::spawn() requires a future that owns its data, and that's just a fact - how you use it just doesn't enter the picture, or in other words the compiler doesn't even attempt to be smart enough to override such bound even where it seems safe to do so.

The other response is that what you're saying is not actually true. Yes, you immediately await the result, but that doesn't guarantee that the future passed to spawn() will not outlive the current scope. Awaiting a future just means that if the awaited future chooses to suspend, the async function that awaits it suspends along with it. The outer future created by the async function may be dropped before it's awaited to completion, in which case the scope disappears while fut1 is still running. For example:

// let's assume this function were allowed to compile
async fn foo() {
    let mut i = 0;
    tokio::spawn(async {
        sleep(1).await;
        i = 1;
    }).await;
    assert!(i == 1);
}

// this function is safe and compiles
async fn bar() {
    {
        // create the foo() future in an inner scope
        let fut = foo();
        // spin up the future created by `foo()` by polling it just once
        Box::pin(fut)
            .as_mut()
            .poll(&mut Context::from_waker(&futures::task::noop_waker()));
        // leave fut to go out of scope and get dropped
    }
    // what memory does `i = 1` modify after 1s?
}
Ozoniferous answered 20/12, 2022 at 15:53 Comment(2)
Well, in your example spawn() doesn't run, but the idea is clear.Yacht
@ChayimFriedman Good point. I've now modified the code to actually poll the future and therefore spin it up and cause spawn() to get executed. Although it makes the code more complex, I think it makes sense to showcase the actual issue.Ozoniferous

© 2022 - 2024 — McMap. All rights reserved.