How does async and await work in Python?

From what I've known, async and await allows code to be executed while waiting for another request to arrive instead of waiting for all requests to be completed. Can someone please example more in detail? Thanks. :D

Comments (2)

Mark's photo

This applies to Python (although it uses pseudocode), but it's similar in most languages.

Yes, it's a way for the thread to not sit idle while waiting for something (usually IO like files, database or network).

When you read a file synchronously, your code does not do anything while the disk is busy. This means that for programs with a lot of IO, the speed of your application is limited by the IO, since you never get close to 100% CPU use.

The solution is to not wait for the IO to complete, but just get back to it when it's ready.

The "original" solution (for which the language does not need to change, which is perhaps the reason for its popularity), is to provide some function that will be run when the IO is ready, then do the IO in the background while your program continues.

(The other obvious solution is to have many threads, but race conditions are horrible, and thread switching isn't as fast jumping to different code in the same thread).

For the computer, this totally solves the problem. It doesn't need to wait for IO, can reach 100% CPU, gets a sign when the IO is ready... Perfect.

But for humans, it quickly becomes messy. Code is much easier to read if the lines are in the same order as the execution. But if you write something like (pseudo-language)

print "start of program!"
open("my_file.txt").read(when_ready = function(content) {
        print "file read!"
print "after!"

This probably prints

start of program!
file read!

Additionally, it's hard to get content out of the function. You basically have to do the entire rest of the program from that function.

So to make this whole "don't wait for IO" more human-friendly. So some languages introduce async/await and do all that bookkeeping in the background. So the above becomes.

print "start of program!"
content = await open("my_file.txt").read()
print "file read!"

Looks much better right? Note though, that the flow gets interrupted at await, so your program state may have changed before and after the file are read. I.e.

print "start of program!", global.my_nr
content = await open("my_file.txt").read()
print "file read!", global.my_nr

May show

start of program! 1
file read! 2

But this happens only at await. So it's much less invasive than 'real' threading.

Then there's fancy stuff like waiting for several files to all be ready, or for the first of several files to become available. But the basic idea is the same: compiler magic to make callbacks convenient.

It's not just for IO. You can also do e.g. heavy CPU stuff in an awaitable task, if you want your main thread to go on while it runs.

Note that asynchronicity in this way is not typically parallel. Okay, it is a little: the file reading and the main thread happen in parallel.

But although different 'flows' of the main thread are intertwined, they are never running at the same time. One piece runs until await #1, then another one runs until await #2, and await #1 continues.

But at the other hand, there is nothing stopping anyone from having multiple threads doing this await switching. (Well, Python's GIL makes it unattractive... but you can!) Or even to distribute the await flows over threads dynamically. But then you get all the problems of multithreading back, so many (like Node) don't do it.

To be technical: async/await is a way to have continuation passing style behaviour but pretend that you're writing normal-looking code with returns. It is one of several ways to solve IO-bound performance, perhaps the most fast and elegant way if language- and library support are available.

Dandy Cheng's photo

Well explained! Thanks!