Louis Lac
Louis Lac

Reputation: 6426

Why doesn't the `tokio::join!` macro require the `await` keyword in Rust?

In Rust (and other programming languages as well), the await keyword is used to indicate a suspension point in the body of an asynchronous function. However, I noted that contrary to other programming languages (Python gather, Swift withTaskGroup, JavaScript Promise.all, etc.), Rust does not require and will refuse the presence of an await after the Tokyo::join! macro.

For instance in this async function there is no await involved after join!:

use tokio::{io::Result, time::{sleep, Duration}, join};

async fn work() -> String {
    sleep(Duration::from_secs(2)).await;
    String::from("Work done")
}

#[tokio::main]
async fn main() -> Result<()> {
    println!("Awaiting…");
    let (o1, o2) = join!(
        work(),
        work(),
    );  // No `.await`
    println!("{}, {}", o1, o2);
    Ok(())
}

Is there a technical reason why the join! macro does not return some sort of aggregated Future or join handle that could be awaited on?

This implicit await is confusing since this asynchronous function appears to not have any suspension point at first sight (thus not requiring to be async) and I think that this may defeat the purpose of the async-await pattern that seeks to make suspension points explicit.

Upvotes: 2

Views: 500

Answers (1)

Kevin Reid
Kevin Reid

Reputation: 43851

If you expand the macro (this can be done on the Rust Playground with the Tools menu), you'll see code that looks like this:

let (o1, o2) = {
    // [some imports]
    let mut futures = (maybe_done(work()), maybe_done(work()));
    // [some more setup]
    poll_fn(move |cx| {
        // [innards]
    }).await
};

That await at the end is the only await in the expansion. So, there is no technical reason why the macro couldn't have been written to not contain the await. However, let's look at the history.

The join macro was introduced in commit 7079bcd60975f592e08fcd575991f6ae2a409a1f (PR #2158) and there wasn't much discussion there, but it had a predecessor in the futures library, introduced in commit d67e2936c21a4d663814e38b06ce38d85bb02e9b (PR #1051) there.

One thing to note about that original implementation is that, unlike the one that is current in Tokio, it is heavily based on being inside an async context (the poll! and pending! macros depend on it):

...
loop {
    let mut all_done = true;
    $(
        if let ::core::task::Poll::Pending = poll!($fut.reborrow()) {
            all_done = false;
        }
    )*
...

Additionally, there was some discussion in the PR:

MajorBreakfast commented on Jun 28, 2018

  • There probably should be some clear distinction between macros intended for use in async functions and other macros. Macros that internally use await should be marked somehow. Alternatively they could both use an async block and require an await around them await!(join![a, b]). This just looks so bad because await! is currently a macro, but it won't be in the future. The mental model is easier though and you could use it everywhere not just in async functions.

cramertj commented on Jun 28, 2018

@MajorBreakfast select! needs to do the await! internally (rather than providing a future as a result) in order to allow the compiler to be smarter about control-flow (e.g. allowing return to work normally, enabling the initialized-ness checks to understand the different conditions under which a break can occur, etc.). Since select! doessn't return a future, I thought it would be confusing for join! to return a future-- the two seem like somewhat natural doubles, and it seems surprising that they would work differently in that respect.

So, select! benefits from not introducing a new async block but incorporating itself into the caller's async block, and join! was designed to have the same character as select!, even though it itself does not benefit.

Upvotes: 3

Related Questions