Using Python’s Asyncio to Asynchronously Run Existing Blocking Code in 2020

Using Python’s Asyncio to Asynchronously Run Existing Blocking Code in 2020

TL;DR Code Snippet for Python >=3.7:

import asyncio
import time

def existing_blocking_method():
    print('going to sleep')
    time.sleep(2)
    print('waking up')

async def main():
    loop = asyncio.get_running_loop() # Only exists in python 3.7
    await loop.run_in_executor(None, lambda: existing_blocking_method()) # These run consecutively, as run_in_executor returns a Future, which is a callback that eventually returns the result.
    await loop.run_in_executor(None, lambda: existing_blocking_method())

async def run_concurrently():
    result = await asyncio.gather(main(), main()) # You can see that these all run concurrently as they are concurrent tasks
    # Note that these are all running on the same thread, except for the executor of course.

asyncio.run(run_concurrently())# Note that this method exists in Python 3.7 only, and it is a very good way to initialize your asyncio calls as it does not require getting the event loop

TL;DR Code Snippet for Python <=3.6:

import asyncio
import time

def existing_blocking_method():
    print('going to sleep')
    time.sleep(2)
    print('waking up')

async def main():
    loop = asyncio.get_event_loop()
    await loop.run_in_executor(None, lambda: existing_blocking_method()) # These run consecutively, as run_in_executor returns a Future, which is a callback that eventually returns the result.
    await loop.run_in_executor(None, lambda: existing_blocking_method())

loop = asyncio.get_event_loop()
result = await asyncio.gather(main(), main())  
# Note that these are all running on the same thread, except for the executor of course.

Python and Asyncio

The appeal of Python is that it is extraordinarily straightforward – allowing scripting, experimentation, and “quick & dirty” code to be written using one of its thousands of impressive libraries to be written with speed and ease. It’s very quick to Google something with “+ Python” in the search to come up with code and an explanation of how to use it very quickly. As you may have found out, information about Python’s Asyncio does not follow this paradigm. The best you may be able to find is generally the official Docs which can certainly be intimidating. More importantly, if you’re just looking to experiment, learn, and try it out, there may seem to be too large of a barrier to entry for it to be worth it.

The reason for this is clear. Asyncio is new and it is constantly evolving. In fact, sub-versions of python differ in large ways in the usage of Asyncio, and even the current documentation warns that certain methods may be subject to refactoring and change. I hope to shed some light on one usage of asyncio in this post, which I hope can be a stepping stone to your full understanding of how to use the library and integrate it with your existing code. Asyncio is a very powerful tool and it is fairly intuitive as long as you read the correct information. As I stated before, information from a year ago or earlier may be dated, and it may be inaccurate simply due to the quickly evolving nature of Asyncio. The feature that I will outline is how to asynchronously run existing blocking code using Asyncio.

Asyncio Brief Background

Asyncio uses the syntax async and await as a syntactic sugar for an earlier-used Python yield from keyword along with decorators which defined coroutines. For more information about this, see this article.

All that you need to know is that Asyncio enables you to run your code on a single thread concurrently, by interspersing methods before they have completed. Asyncio intersperses the methods when they make an async call. That is, if you have 2 methods running but they both rely on waiting for an exterior factor (i.e. user input, http response, or a specified wait time), you can tell Asyncio that you will be waiting on that exterior source for feedback, and in the meantime it can run other code. At this time, Asyncio checks its Event Loop for the next available point to resume execution and resumes it. In this way, you can emulate concurrency using a single thread, and you can maximize your code efficiency.

Methods which can be awaited are defined with the async def keyword instead of the normal def keyword. Additionally, code which is run using Asyncio has to be initialized in a specific way, which varies based on the version of Python you’re using.

Existing Blocking Code

Given the fact that Asyncio is designed to run blocking code and yield the event loop while it’s blocked, a common and important application of Asyncio would be to run existing blocking code in an asynchronous environment. For example, if you have the blocking code:

def existing_blocking_method():
    print('going to sleep')
    time.sleep(2)
    print('waking up')

You might imagine that instead of sleeping, this code is making a http call. To run this code asynchronously, Asyncio provides Executors. When you use the method asyncio.run_in_executor, Asyncio executes the given task in a different thread or process (depending on the type of executor), and it yields the event loop until the execution is complete. The arguments to asyncio.run_in_executor are 1) The type of executor, 2) A function to execute, and 3) the *args of the function. A good trick to simplify this, at least in my opinion, is to use the blocking method call as you would normally, but use its execution as the statement inside a Python Lambda. For example:

await loop.run_in_executor(None, lambda: existing_blocking_method())
This is really the crux of the solution for asynchronously running existing blocking code in Asyncio. This method will run the existing blocking method in a different thread (by default) and yield the loop back for other asynchronous tasks to run while the blocking code runs in the background.

The cons of this approach

It is very important to keep in mind that running code in this way should be avoided as much as possible, in favor of libraries specifically designed and optimized to work with Asyncio, such as aiohttp. This is due to the fact that running code in another thread or process using an executor requires a context switch which involves overhead. By contrast, asynchronous libraries will be designed to mitigate the performance issues caused by this context switch by avoiding it as much as possible. So, before using an executor, see if you can replace that functionality using a special asynchronous library. If not, it is still much better to use an executor than to run your code synchronously in many cases.

Putting it all together

Note that this section will use code which runs in Python 3.7+. For code for earlier versions, see the snippets at the beginning of the article.

Let’s say that you have an existing blocking function that needs to be run twice by your main method, and you main method itself also needs to be run twice. A code snippet for this would be as follows:

import time

def existing_blocking_method():
    print('going to sleep')
    time.sleep(2)
    print('waking up')

def main():
    existing_blocking_method()
    existing_blocking_method()

main()
main()

Clearly, this code snippet would take 8 seconds, as each main call will take 4 seconds due to 2 blocking calls that take 2 seconds each. If we replace this with Asyncio as we have discussed earlier:

import asyncio
import time

def existing_blocking_method():
    print('going to sleep')
    time.sleep(2)
    print('waking up')

async def main():
    loop = asyncio.get_running_loop() # Only exists in python 3.7
    await loop.run_in_executor(None, lambda: existing_blocking_method()) # These run consecutively, as run_in_executor returns a Future, which is a callback that eventually returns the result.
    await loop.run_in_executor(None, lambda: existing_blocking_method())

async def run_concurrently():
    result = await asyncio.gather(main(), main()) # You can see that these all run concurrently as they are concurrent tasks
    # Note that these are all running on the same thread, except for the executor of course.

asyncio.run(run_concurrently())# Note that this method exists in Python 3.7 only, and is a very way to initialize your asyncio calls as it does not require getting the event loop

This code snippet will take only 4 seconds. The main method is run twice concurrently. During each execution, the existing_blocking_method will be run and the result will be awaited as a future. Remember that this all occurs in the same thread; thus, while the execution of the first main() call’s existing_blocking_method runs, the second main() is given the loop and executes its first existing_blocking_method as well. The same occurs for the second existing_blocking_method call for each main().

Conclusion

Asyncio is a very powerful library which is evolving and improving rapidly before our eyes. I hope that this post captures a currently up-to-date method of not only executing code asynchronously using Asyncio, but also using Asyncio to run existing blocking code in the most efficient possible way.

Feel free to leave any comments that you have!

Further Reading

Why doesn’t Asyncio always use executors?

ThreadPoolExecutor VS ProcessPoolExecutor

Leave a Comment