Programmer's Python Async - Asyncio
Written by Mike James   
Monday, 10 October 2022
Article Index
Programmer's Python Async - Asyncio
Await
Tasks

pythonAsync180

Await

As it stands our coroutine might as well be a standard function as it doesn’t suspend and resume its operation. To suspend a coroutine you have to use the await keyword to pause the coroutine while an awaitable completes. At the moment you can await a coroutine, a Future or a Task- the Future and the Task are described in detail later.

  • When you await a coroutine it starts running and the awaiting coroutine is suspended until the awaited coroutine completes and returns a result.

  • A Future is awaitable because at some point in the future a function that it is associated with it will complete and resolve the Future with either a result or an exception.

  • A Task is a subclass of Future so it is just a special case. In addition to being a Future, it has a coroutine attached which is run and automatically resolves the Future according to its result.

In each case an await suspends the awaiting program and this means it can only be used within a coroutine, i.e. the only code in Python that can be suspended and resumed. Once you have a coroutine running you can use await within it and within any coroutines it awaits. This means that you have to use asyncio.run to get a first coroutine running but after this you can use await to run coroutines or Tasks.

Most asyncio programs are organized so that there is a single asyncio.run instruction at the top level of the program and this starts a coroutine – often called main – which then runs the rest of the asynchronous program by awaiting other coroutines. That is, a typical asyncio program is:

async def main():
	call other coroutines using await
asyncio.run(main())

The call to asyncio.run sets up the event loop as well as starting main running. You can call ordinary, i.e. non-coroutine, functions from within coroutines, but these cannot use await. Only a coroutine can use await, for example:

import asyncio
async def test1(msg): print(msg) async def main(): await test1("Hello Coroutine World")
asyncio.run(main())

Notice that even though main now awaits the test1 coroutine there is no new behavior. The program would work in exactly the same way with functions replacing coroutines. The reason is that none of our coroutines actually release the main thread, they simply keep running.

There are two distinct things that can occur when you await another coroutine. Some coroutines hold onto the main thread and continue to execute instructions until they finish – they are essentially synchronous coroutines. Some release the main thread while they wait for some operation to complete and only these are truly asynchronous coroutines. At the moment we only know about synchronous coroutines.

Awaiting Sleep

Later we will meet asynchronous coroutines that do release the thread while they wait for I/O operations to complete, but these are complicated by what they actually do. What we need is a simple asynchronous coroutine that releases the main thread and does little more. The simplest asynchronous coroutine is asyncio.sleep is:

asyncio.sleep(delay, result = None)

which returns after delay seconds and returns result if specified.

The asyncio.sleep coroutine is not the same as the time.sleep function used in previous chapters and it is important to understand the difference.

The time.sleep function suspends the current thread for the specified amount of time. If you were to use it in a single-threaded environment the result would be that the one thread that you were depending on to do the work would be frozen and so would the event loop. In fact time.sleep is a very good way to keep the thread busy and so simulate a coroutine that doesn’t give up the thread.

Compare this to asyncio.sleep which doesn’t suspend the thread at all - it suspends the coroutine. The main thread stops running the coroutine and returns to the event loop to find another coroutine to run. When the time delay is up and the main thread next visits the event loop for more work then the suspended coroutine is restarted. Of course, this means that the coroutine might be suspended for longer than the specified time and this is usually the case. The coroutine is restarted when the main thread is free to run it and the time delay is up.

The asyncio.sleep function suspends the current coroutine and not the thread. This means that asyncio.sleep really does mimic what happens when other, more complex, async functions are used and hence it is a good but simple example. You can even use the result parameter to simulate returning an object.

A standard idiom is to call sleep with a value of zero seconds:

await asyncio.sleep(0)

This gives up the thread to the event loop with the minimum delay if there is nothing to be done in the event loop queue. That is, all that happens is that the main thread is freed and, if there is nothing waiting to be executed in the event loop, it returns at once to running the coroutine. This gives the event loop a chance to run other coroutines and it is a good idea to include any coroutine that runs for a long time. The call sleep(0) is equivalent to DoEvents in other languages, i.e. an instruction to process the event loop’s queue.

We can easily add an await for 10 seconds to our example:

import asyncio
async def main(myValue):
print("Hello Coroutine World")
await asyncio.sleep(10)
return myValue
result= asyncio.run(main(42))
print(result)

There is now a 10-second delay between displaying Hello Coroutine World and the result, i.e. 42. In this case the main thread is freed when the await starts and has 10 seconds in which to run any other coroutines waiting in the event loop. In this case there aren’t any and so it just waits for the time to be up. We next need to know how to add coroutines to the event loop so that they can be run when the main thread is free.



Last Updated ( Monday, 10 October 2022 )