Differences between Python's `await` and Rust's `await`

I used to think they are both the same, I still think conceptually they are the same but I need some affirmations from the army of Rust chads here.

Consider this piece of Python codes:


import asyncio

async def async_hello():
    return 'hello '
    
async def async_world():
    return 'world'
    
async def main():
    hello = await async_hello()
    world = await async_world()
    print(hello + world)
    return hello + world
    
asyncio.run(main())

The Question

It prints "hello world", unsurprisingly, but at each await point, e.g. hello = await async_hello(), does the coroutine created by calling async def main() returns (or yields) back the control to the runtime scheduler provided by asyncio?

Context

In Rust, it appears that this is the case, that each await points yields back to the caller, or the runtime, e.g. tokio runtime, an anonymous Future type that implements the Future trait on which .poll(...) can be called on.

This question stems from an overly repeated dogma that the Python thread 'blocks' at each await point in Python, which is of course, markedly different in Rust, i.e. in Rust, each await point does not block.

I am asking this question in the Rust forum because it is far likely that Rustaceans to know about async Python than async Pythonista to know about Rust. :joy:

1 Like

It must (given that asyncio is single-threaded by default and executes the subroutines on the same thread as the event loop) or otherwise, who would drive the coroutine to completion? The awaitable must yield back to the executor for it to do the necessary work, like checking OS file descriptors and updating timers (running the event loop). From the docs I linked above:

An event loop runs in a thread (typically the main thread) and executes all callbacks and Tasks in its thread. While a Task is running in the event loop, no other Tasks can run in the same thread. When a Task executes an await expression, the running Task gets suspended, and the event loop executes the next Task.

1 Like

Nitpick - await yields back to executor if there is actually something to await for, that is, futures::ready().await does not yield, it continues execution immediately.

3 Likes

That's true, I am assuming that both await points return the Pending variant.

One big difference

Redacted, cf edit history

1 Like

f = bar() does not start executing immediately in your Python snippet. f is just a coroutine object doing nothing if not pulled by either awaiting it or converting it to a Task.

2 Likes

I might be mixing things up with C# and YavaScript, thanks for the correction

1 Like

JavaScript's async is very different because it is not based on coroutines at all. A JavaScript promise is not any kind of coroutine; instead, it is a handle to a result value that will be provided later, like the receiving side of Rust's async oneshot channels except shareable. Scheduling the code that resolves the promise is handled completely separately; the basic operation is "run this closure when you see this promise resolve" (then()) and async functions are effectively built out of that.

1 Like

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.