Async code actually internally works in a similar way to generators and
yield. An awaitable implements an interface almost identical to iterators, except it’s got different method names to distinguish between the protocols.
async def makes the function return an awaitable, which can have
__anext__() called to execute part of it up to the next
await call. That itself acts sorta like
yield from, in that it then passes through the
__anext__ calls, until that awaitable terminates.
The role of the async lib’s
run() function is to contain a while loop, which picks tasks that are ready to execute and calls
anext() to run them. When you call library methods that do things like sleep, do IO, schedule tasks etc those functions don’t call await, but instead “yield” back some special value. That passes back through the call chain, and then the run function receives it and is able to use it to decide how the task should be paused - for instance to only re-run after the delay has elapsed, or the I/O has completed, etc.