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?
}
tokio::spawn
is implemented differently depending on whether the futures are immediatelyawait
ed or not. It is defined to requireFuture + Send + 'static
for its argument. – Macrospore