Reading an AsyncIO Future

An AsyncIO coroutine executes each line in order until it encounters an await statement, at which point, it returns control to the event loop. The event loop then executes any other tasks that are ready to run, including the one that the original coroutine was waiting on. Whenever that child task completes, the event loop sends the result back into the coroutine so that it can pick up execution until it encounters another await statement or returns.

This allows us to write code that executes synchronously until we explicitly need to wait for something. As a result, there is no nondeterministic behavior of threads, so we don't need to worry nearly so much about shared state.

It's still a good idea to avoid accessing shared state from inside a coroutine. It makes your code much easier to reason about. More importantly, even though an ideal world might have all asynchronous execution happening inside coroutines, the reality is that some futures are executed behind the scenes inside threads or processes. Stick to a share nothing philosophy to avoid a ton of difficult bugs.

In addition, AsyncIO allows us to collect logical sections of code together inside a single coroutine, even if we are waiting for other work elsewhere. As a specific instance, even though the await asyncio.sleep call in the random_sleep coroutine is allowing a ton of stuff to happen inside the event loop, the coroutine itself looks like it's doing everything in order. This ability to read related pieces of asynchronous code without worrying about the machinery that waits for tasks to complete is the primary benefit of the AsyncIO module.