rustrust-tokio

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


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 ?


Solution

  • 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
            pin!(fut).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?
    }
    

    It's this concern that makes async equivalent of scoped threads fundamentally unsound.