Demystifying Python's Asyncio, Powering Concurrent and Asynchronous Code


In Python, there is a built-in Python library, asyncio, managing such tasks has become more performant and efficient. So why do we need asyncio and what does it bring to the table? Let’s dive into it.

Why do we need asyncio?

Asyncio brings about a significant boost in performance for IO-bound programs, meaning applications that spend most of their time waiting for Input/Output or other operations, such as network or file operations. This is made possible with the help of “non-blocking” code and async/await syntax. Instead of being idle and wasting valuable CPU cycles during these waiting periods, applications coded with asyncio are getting meaningful work done, thus improving overall performance.

Additionally, asyncio provides better resource utilization. It being single-threaded allows the running of multiple tasks concurrently on a single thread, delivering a significantly more resource-friendly solution compared to creating multiple threads or processes.

In the context readability and simplicity of the code, asyncio makes the cut too. It wraps the complex concepts of callbacks, thread/process management with the async/await syntax making it easier for developers to write, read, and understand concurrent code.

If your application needs to handle multiple tasks or connections simultaneously like in the case of servers, asyncio truly shines. Given its resource efficiency and concurrent capabilities, it’s an ideal choice for such applications.

Writing asyncio code: Best Practice

The suggested and most effective way of writing asyncio code is by leveraging asynchronous operations, denoted by the async keyword, and using the await keyword for IO-bound tasks.

Consider this example below:

import aiofiles

async def async_read_file(file_name):
async with aiofiles.open(file_name, mode='r') as f:
data = await f.read()
return data

In this function, the ‘await’ keyword tells asyncio to pause execution of the current task, whilst waiting for IO. However, unlike time.sleep(), it won’t block other tasks in the event loop.

Remember, asyncio’s central idea is non-blocking, cooperative multitasking, so sticking to non-blocking functions and libraries that support asyncio’s asynchronous operations is your best bet.

Fallback method using run_in_executor

But what if the I/O operation doesn’t have an async equivalent, or we’re stuck with a legacy codebase of synchronous blocking functions? For these instances, asyncio provides a fallback method, asyncio.run_in_executor(). This method runs your blocking operations in a separate thread allowing your asyncio code to keep running without being blocked.

Let’s see how it’s done:

import asyncio
from concurrent.futures import ThreadPoolExecutor
import time

def sync_function(seconds):
time.sleep(seconds)
return "Finished sleeping"

async def main():
executor = ThreadPoolExecutor(max_workers=3)
loop = asyncio.get_event_loop()
result = await loop.run_in_executor(executor, sync_function, 3)
print(result)

asyncio.run(main())

This allows the blocking function to seem as if it were a normal async function without blocking the main event loop. But bear in mind, while run_in_executor() can be a life-saver with blocking code, the ideal asyncio design is to stick as much as possible with non-blocking, async-native functions.

Wrapping up

Asyncio empowers Python to handle concurrent tasks in a more efficient and performant way. By using asynchronous functions wherever possible, and knowing how to deal with blocking functions, you can write code that is efficient, fast, and capable of handling more tasks than before. However, remember that asyncio serves IO-bound tasks best and for CPU-bound tasks, following the traditional multi-threading or multi-process methods might be more effective.


Author: robot learner
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint policy. If reproduced, please indicate source robot learner !
  TOC