In this thread, there seems to be a dispute between people who believe that await
is the only thing you can do in an async
block that has a special control-flow property, and people who believe that any other async fn
called from within an async
block has the same property.
I’m not 100% sure about the state of play in Rust because I haven’t been following the larger design carefully, but in other languages with async+await, I can say pretty confidently that the former perspective is correct. Calling an async function, by itself, always gives you back a future, even if you’re already in an async context. This is necessary, because it lets you choose to await the futures in a different order than you created them, or in no particular order. A concrete example might help; I’m deliberately writing it in Python because (a) I know how it works for sure in Python, and (b) it lets me shove the syntax argument completely to the side.
async def do_three_things_simultaneously():
f1 = start_operation_1()
f2 = start_operation_2()
f3 = start_operation_3()
return await asyncio.gather(f1, f2, f3)
The only suspension point for this function is at the await
, even though start_operation_[123]
are themselves async functions. You don’t have to worry about concurrent code getting a chance to run before all three futures are created. In fact, the Python implementation could optimize this into an ordinary function that returned the future produced by asyncio.gather
without awaiting it first, and no correct caller could tell the difference.
(Despite this, I still firmly believe that, in Rust, await
should be treated as a method and written as .await()
, because my perspective is that the primary point of async+await is to conceal the state machine that an async
block gets compiled into, which means you should think of await
as not having any special control flow property. Rather, you should imagine that you are writing synchronous code for a cooperative multitasking environment, in which .await()
is the only system call that can block and/or deliver a cancellation request.)
(Hmm, let’s look at translating the above into Rust with either postfix .await
or prefix await
. Modulo some type annotations, we would have
// RT1, RT2, and RT3 are the return types of start_operation_[123]
async fn do_three_things_simultaneously_postfix() -> (RT1, RT2, RT3) {
aio::gather((
start_operation_1(),
start_operation_2(),
start_operation_3(),
)).await
}
async fn do_three_things_simultaneously_prefix() -> (RT1, RT2, RT3) {
await aio::gather((
start_operation_1(),
start_operation_2(),
start_operation_3(),
))
}
fn do_three_things_simultaneously_in_caller() -> Future<(RT1, RT2, RT3)> {
aio::gather((
start_operation_1(),
start_operation_2(),
start_operation_3(),
))
}
All three are formally equivalent. I think it is more obvious that the postfix
version is equivalent to the in_caller
version, I think it is more obvious that await
is a potentially expensive operation in the prefix
version, and I think postfix .await()
would communicate both things simultaneously.)