Reputation: 6426
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 await
ed 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
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 anasync
block and require anawait
around themawait!(join![a, b])
. This just looks so bad becauseawait!
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 theawait!
internally (rather than providing a future as a result) in order to allow the compiler to be smarter about control-flow (e.g. allowingreturn
to work normally, enabling the initialized-ness checks to understand the different conditions under which a break can occur, etc.). Sinceselect!
doessn't return a future, I thought it would be confusing forjoin!
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