Using Asyncio: Some Examples and Patterns

Since Python 3.4, there has been inbuilt support for asynchronous programming in Python. Unfortunately, the Asyncio library has received a lot of criticism, with much of this criticism being rather unjust. These days many of the asynchronous web frameworks and database tools out there require little interaction with the underlying Asyncio library and simply require an understanding of the async await syntax. In this post, we are going to go into how you can use the Asyncio library in a more general sense. As pointed out by Yury Selivanov, a significant chunk of developers don’t need to worry about these internals and can simply benefit from async/await syntax.

Helper Function

We are going to use the same simple function throughout the following examples. The function simply creates a client and makes a http request. This simple function will help us demonstrate the different ways we can use the underlying Asyncio library. The helper function pulls in a dependency, namely Aiohttp and will be used throughout the following examples. Note that we aren’t properly handling errors here and simply passing back a string stating the request returned an exception. Any production code would need to do a better job with error handling.

As Completed

If we need to collect a group of results before proceeding with our script or program, using as completed can be a good option. This allows us to asynchronously run a number of coroutines at once, with the results being returned when they are ready. An example can be found in the following code snippet.

However, this is not always appropriate as we may have to wait a long time to receive all of the returned results particularly if the list of URLs we wanted to retrieve was particularly large. If we want to run a group of coroutines and gather the results, as completed gives one way of doing so.

Using Futures And Callbacks

Asyncio also has the concept of futures this means that you can register code to run after an asynchronous function has completed. This has very little utility in day to day asyncio programming, and is highlighted as such
Typically Futures are used to enable low-level callback-based code (e.g. in protocols implemented using asyncio
transports) to interoperate with high-level async/await code. “

The above code creates a future then registers a callback which will make use of the result returned through the future. We are able to register multiple callbacks should we want, allowing us to use the results of async function in multiple places.

However, callback based programming can be very confusing and should ideally be avoided where possible. This means that futures and callbacks have limited utility in day to day async programming.

Using Tasks

Asyncio also gives users the ability to create tasks directly in the event loop. This allows for the creation of fire and forget tasks which run in the background but do not need to be awaited. However, such fire and forget tasks are considered an anti-pattern and should not be used for a number of reasons. Fire and forget tasks will be cancelled if the event loop is exited and there is no easy way to handle errors which can occur within these tasks.

The below example demonstrates both waiting for a task and the creation of a fire and forget task for reference purposes.

Waiting Results

Asyncio also provides a way to run a number of co-routines and await the results as they return. The ‘await’ function works similarly to asyncio as completed, but gives the user the ability to continually poll the futures for whether they are completed. Again, this is particularly useful if you want to run a bunch of futures together at one time.

Wait provides one of the easiest ways to run a bunch of coroutines together and gather the results. It also provides users the ability to continually poll futures and allows for users to handle any exceptions that occur in the future should they wish. It is also possible to pass a timeout to the wait function which will cancel any futures which are not completed within the timeout period.

Gather

Gather provides a very similar interface to wait but does not provide users the ability to poll the tasks. Gather is commonly used to run a number of different groups of coroutines often in the main function of an async program. Gather provides one of the simplest APIs and should be used whenever you simply want to run a group of couroutines and gather all the results, as the name would suggest.

As you can see gather provides us with a very easy way to return results from a number of different coroutines by simply feeding them into the gather using a *.

Gather is one of the most commonly used lower level APIs and is extremely useful when writing programs and scripts which make use of the asyncio library.

Conclusions

The asyncio library provides a number of different lower level APIs for running and handling results from coroutines. When using many of the popular asyncio web frameworks using these APIs tends to be unnecessary with users able to simply use async and await to produce performant servers.

However, knowing how to use these core APIs will allow you to write your own asyncio programs and scripts which take advantage of all of what asyncio has to offer.