Why doesn't this async code print?

Why does the following code just print "salutations"? Why do async blocks desugar in such a way that code before the first await in the async block is deferred until the future is polled? Is this behavior guaranteed? It seems like it could constrain the lifetime of the future being created.

async fn g() {
     println!("greetings");
}

fn h() -> impl std::future::Future<Output = ()> {
    println!("salutations");
    async {}
}

fn main() {
    let f = async { println!("hello"); };
    drop(f);
    drop(g());
    drop(h());
}

(Playground)

Output:

salutations

Errors:

   Compiling playground v0.0.1 (/playground)
    Finished dev [unoptimized + debuginfo] target(s) in 0.60s
     Running `target/debug/playground`

Async functions are lazy: rfcs/2394-async_await.md at master · rust-lang/rfcs (github.com)

Emphasis added:

Async functions work differently from normal functions. When an async function is called, it does not enter the body immediately. Instead, it evaluates to an anonymous type which implements Future. As that future is polled, the function is evaluated up to the next await or return point inside of it (see the await syntax section next).

There is some history to this that I can't quite find right now, but this comment agrees that it constrains all captured lifetimes and suggests that this is intentional.

edit: This is the discussion that I was looking for: async/await notation for ergonomic asynchronous IO by withoutboats · Pull Request #2394 · rust-lang/rfcs (github.com)

1 Like

Because you haven't used any async runtime which may execute anything.

Because it's the only way to allow you to do what you did: write async which is not dependent on any async runtime.

Obviously.

Indeed. But making async runtime pluggable was deemed important enough that it was done. And without async runtime you can not actually run async functions. Which, pretty much ensures that async would work in a way it does.

Other languages, which actually have rich runtime, including support for async may do other choice, but Rust couldn't have anything else for obvious reason: before you link in async runtime there are literally nothing that may make you async code runnable… yet it still have to be compileable… somehow.

I think this is more subtle than it appears and is certainly not obvious.

From @parasyte's second link:

This is required for stacking Future combinators without allocation in-between each one, as after the first poll they can't be moved.

To demonstrate this, here is a concrete example of why the design demands that async fns are lazy:

async fn foo() {
    let owned_data = [1, 2, 3, 4, 5];
    for &i in owned_data.iter() {   // iterator has a reference to `owned_data`
        some_other_async(i).await;
    }
}

As soon as .iter() is called, which happens before the first await point, the std::slice::Iter iterator is owned by the future and also borrowing from the future. Therefore, this code can't be executed until the future is pinned, which isn't required until the first poll().

In principle, Rust could define the async code transformation so that code that doesn't have any borrows of local variables does run before the future is polled, but that would be a more complex and potentially surprising execution ordering. The current one is simple: nothing happens until the first poll. If you want to run something before the first poll, you write a function that returns async {} instead of an async fn.

4 Likes

Thanks for this explanation.

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