You can run a blocking function in asyncio via the asyncio.to_thread() function.
In this tutorial, you will discover how to execute blocking functions in new threads separate from the asyncio event loop.
Let’s get started.
The problem with Blocking the Event Loop
A problem with asyncio is that if a blocking call is executed in the event loop, it will halt the application.
This is because only one coroutine can run at a time.
Recall that a blocking call is a function call that does not return until it is complete.
Instead of simply suspending the current coroutine, the call will suspend the thread in which the coroutine is running.
Examples include blocking I/O such as reading or writing from a file, or sleeping such as a call to time.sleep().
You can learn more about blocking function calls in the tutorial:
How can we execute a thread blocking function call in asyncio?
Run loops using all CPUs, download your FREE book to learn how.
Run Function in New Thread with to_thread()
We can execute thread blocking function calls in asyncio using the asyncio.to_thread() function.
Asynchronously run function func in a separate thread.
— Coroutines and Tasks
This function was added to the asyncio module in Python 3.9.
It will take a function call and execute it in a new thread, separate from the thread that is executing the asyncio event loop.
It allows the asyncio event loop to treat a blocking function call as a coroutine and execute asynchronously using thread-based concurrency instead of coroutine-based concurrency.
The asyncio.to_thread() function is specifically designed to execute blocking I/O functions, not CPU-bound functions that might also block the asyncio event loop.
This coroutine function is primarily intended to be used for executing IO-bound functions/methods that would otherwise block the event loop if they were ran in the main thread.
— Coroutines and Tasks
This is a useful function to use when we have an asyncio program that needs to perform both non-blocking IO (such as with sockets) and blocking IO (such as with files).
Now that we know how to execute blocking functions asynchronously in asyncio, let’s look at how to use the to_thread() function.
How to Use to_thread()
The to_thread() function takes the name of a blocking function to execute and any arguments to the function.
It then returns a coroutine that can be awaited to get the return value from the function, if any.
For example:
1 2 3 4 5 |
... # create a coroutine for a blocking function blocking_coro = asyncio.to_thread(blocking, arg1, arg2) # await the coroutine and get return value result = await blocking_coro |
The blocking function will not be executed in a new thread until it is awaited or executed indecently.
The coroutine can be wrapped in an asyncio.Task to execute the blocking function call independently.
For example:
1 2 3 4 5 |
... # create a coroutine for a blocking function blocking_coro = asyncio.to_thread(blocking) # execute the blocking function independently task = asyncio.create_task(blocking_coro) |
This allows the blocking function call to be used like any other asyncio Task.
Now that we know how to use the asyncio.to_thread() function, let’s look at some worked examples.
Free Python Asyncio Course
Download your FREE Asyncio PDF cheat sheet and get BONUS access to my free 7-day crash course on the Asyncio API.
Discover how to use the Python asyncio module including how to define, create, and run new coroutines and how to use non-blocking I/O.
Example of Blocking the Asyncio Event Loop
Before we explore an example of running a blocking function in a new thread, let’s look at an example of running the blocking function directly in the asyncio event loop.
In this example we will define a blocking function call that reports a message, sleeps, then reports a final message. Importantly, the time.sleep() function is used to block the current thread, including any currently running coroutines.
You can learn more about blocking a thread with sleep in the tutorial:
We also define a background coroutine that runs in a loop forever. Each iteration it reports a message and sleeps for half a second.
The main coroutine starts the background task independently. It then calls the blocking function.
The complete example is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
# SuperFastPython.com # example of running a blocking function call in asyncio import time import asyncio # blocking function def blocking_task(): # report a message print('task is running') # block time.sleep(2) # report a message print('task is done') # background coroutine task async def background(): # loop forever while True: # report a message print('>background task running') # sleep for a moment await asyncio.sleep(0.5) # main coroutine async def main(): # run the background task _= asyncio.create_task(background()) # execute the blocking call blocking_task() # start the asyncio program asyncio.run(main()) |
Running the example first creates the main() coroutine and uses it as the entry into the asyncio program.
The main() coroutine runs. It creates the background coroutine and schedules it for execution as soon as it can.
The main() coroutine then calls the blocking_task() function.
This does not suspend the main() coroutine, instead, the call to the blocking_task() function is performed in the event loop and the call to sleep() blocks the current thread.
The background task coroutine cannot run.
Once the blocking call is finished, the background task is given an opportunity to run briefly before the main() coroutine is terminated.
This highlights that a blocking function call in a coroutine or task will not suspend and will block the entire event loop.
1 2 3 |
task is running task is done >background task running |
Next, let’s look at how we can update the example to run the blocking call in a new thread.
Overwhelmed by the python concurrency APIs?
Find relief, download my FREE Python Concurrency Mind Maps
Running a Blocking Function in a New Thread
We can explore how to execute the blocking call in a new thread and not stop the event loop.
In the example below we call asyncio.to_thread() to create a coroutine for the call to the blocking_task() function.
This coroutine is then awaited allowing the main coroutine to suspend and for the blocking function to execute in a new thread.
The complete example is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
# SuperFastPython.com # example of running a blocking function call in asyncio in a new thread import time import asyncio # blocking function def blocking_task(): # report a message print('task is running') # block time.sleep(2) # report a message print('task is done') # background coroutine task async def background(): # loop forever while True: # report a message print('>background task running') # sleep for a moment await asyncio.sleep(0.5) # main coroutine async def main(): # run the background task _= asyncio.create_task(background()) # create a coroutine for the blocking function call coro = asyncio.to_thread(blocking_task) # execute the call in a new thread and await the result await coro # start the asyncio program asyncio.run(main()) |
Running the example first creates the main() coroutine and uses it as the entry into the asyncio program.
The main() coroutine runs. It creates the background coroutine and schedules it for execution as soon as it can.
The main() coroutine then creates a coroutine to run the background task in a new thread and then awaits this coroutine.
This does a couple of things.
Firstly, it suspends the main() coroutine, allowing any other coroutines in the event loop to run, such as the new coroutine for executing the blocking function in a new thread.
The new coroutine runs and starts a new thread and executes the blocking function in the new thread. This coroutine was also suspended.
The event loop is free and the background coroutine gets an opportunity to run, looping and reporting its messages.
The blocking call in the other thread finishes, suspends the background task, resumes the main thread, and terminates the program.
This highlights that running a blocking call in a new thread does not block the event loop, allowing other coroutines to run while the blocking call is being executed, suspending some thread other than the main event loop thread.
1 2 3 4 5 6 |
task is running >background task running >background task running >background task running >background task running task is done |
What’s the difference?
Let’s pause for a moment and consider the difference between this example and the previous example.
In both cases, the main() coroutine waits for the blocking call to finish.
- In the first case, the blocking call suspended the entire thread.
- In the second case, the blocking call is executed in a new thread and only the coroutine was suspended.
The first case prevents any other coroutines from running while the blocking call is blocked and waiting. The second case allows other coroutines to run while the blocking call is blocked and waiting, as seen in the program output.
We now have a good idea of what the to_thread() function is doing, we can now consider the other details when using the function.
Next, let’s look at executing a blocking call that takes arguments in a new thread.
Running a Blocking Function With Arguments in a New Thread
We can explore how to execute a blocking call in a new thread that takes arguments.
In this example, we will update the blocking function to take two arguments. These arguments are provided to the asyncio.to_thread() directly, not in a tuple or dict.
The blocking function will be executed in a new thread and reports the arguments.
The complete example is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
# SuperFastPython.com # example of running a blocking function call in asyncio in a new thread with arguments import time import asyncio # blocking function def blocking_task(value1, value2): # report a message print(f'task is running with {value1} and {value2}') # block time.sleep(2) # report a message print('task is done') # main coroutine async def main(): # create a coroutine for the blocking function call coro = asyncio.to_thread(blocking_task, 100, 'TEST') # execute the call in a new thread and await the result await coro # start the asyncio program asyncio.run(main()) |
Running the example first creates the main() coroutine and uses it as the entry into the asyncio program.
The main() coroutine runs and creates a coroutine to run the background task in a new thread and then awaits this coroutine.
It passes two arguments, in this case, an integer and a string.
The main() coroutine suspends and the new coroutine runs, starts a thread, and executes the blocking function in the new thread. This coroutine too then blocks.
The blocking function runs, reports its message, sleeps, then reports its final message.
The new coroutine finishes and the main() coroutine resumes and terminates the asyncio program.
This highlights how we can pass arguments to a blocking function executed in a new thread.
1 2 |
task is running with 100 and TEST task is done |
Next, let’s look at an example of a blocking call that returns a value in a new thread.
Running a Blocking Function with a Return Value in a New Thread
We can explore how to execute a blocking call in a new thread that returns a value.
In this example, we will update the blocking function to return an arbitrary value.
The main coroutine will wait for the blocking task to complete and will then report the return value.
The complete example is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
# SuperFastPython.com # example of running a blocking function call in asyncio in a new thread with return value import time import asyncio # blocking function def blocking_task(): # report a message print('task is running') # block time.sleep(2) # report a message print('task is done') # return a value return 100 # main coroutine async def main(): # create a coroutine for the blocking function call coro = asyncio.to_thread(blocking_task) # execute the call in a new thread and await the result result = await coro # report the result print(f'Got: {result}') # start the asyncio program asyncio.run(main()) |
Running the example first creates the main() coroutine and uses it as the entry into the asyncio program.
The main() coroutine runs and creates a coroutine to run the background task in a new thread and then awaits this coroutine, expecting a return value.
The main() coroutine suspends and the new coroutine runs, starts a thread, and executes the blocking function in the new thread. This coroutine too then blocks.
The blocking function runs, reports its message, sleeps, then reports its final message, then returns a value.
The new coroutine finishes and the main() coroutine resumes. It retrieves the return value and reports it directly.
This highlights how we can receive and use return values from blocking functions executed in a new thread.
1 2 3 |
task is running task is done Got: 100 |
Next, let’s look at how we might execute a blocking call in a new thread independently.
Running a Blocking Function Independently in a New Thread
We can explore how to independently execute a blocking call in a new thread.
In this example, we update the main coroutine to create and schedule a new task to execute the coroutine for running the blocking function in a new thread.
The main coroutine continues on with other activities, allowing the task to execute independently.
The complete example is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
# SuperFastPython.com # example of running a blocking function call in asyncio in a new thread independently import time import asyncio # blocking function def blocking_task(): # report a message print('task is running') # block time.sleep(2) # report a message print('task is done') # main coroutine async def main(): # create a coroutine for the blocking function call coro = asyncio.to_thread(blocking_task) # execute the call in a new thread independently task = asyncio.create_task(coro) # wait a moment await asyncio.sleep(2.1) # start the asyncio program asyncio.run(main()) |
Running the example first creates the main() coroutine and uses it as the entry into the asyncio program.
The main() coroutine runs and creates a coroutine to run the background task in a new thread.
This new coroutine is then wrapped in a new asyncio.Task and scheduled for execution.
The main() coroutine then suspends, sleeping for a while.
The new coroutine runs and executes the new task asynchronously in a new thread.
The main() coroutine resumes after a while, long enough for the task in a new thread to finish, and terminates the program.
This highlights how we do not have to directly await the coroutine for the blocking function executed in a new thread so that it can be scheduled for independent execution.
1 2 |
task is running task is done |
Next, let’s look at the details of the thread that executes the blocking call.
Example of Reporting Details of New Thread
We can explore the properties of the thread used to execute blocking calls.
In this example, we will update the blocking function to get the current thread via a call to threading.current_thread(). This returns a threading.Thread instance representing the thread executing the function.
The function then reports the details of the thread that is executing the blocking function, including the name and whether it is a daemon thread or not.
The complete example is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
# SuperFastPython.com # example of running a blocking function call in asyncio in a new thread import time import asyncio import threading # blocking function def blocking_task(): # report a message print('task is running') # block time.sleep(2) # report details of thread thread = threading.current_thread() print(f'name={thread.name}, daemon={thread.daemon}') # main coroutine async def main(): # create a coroutine for the blocking function call coro = asyncio.to_thread(blocking_task) # execute the call in a new thread and await the result await coro # start the asyncio program asyncio.run(main()) |
Running the example first creates the main() coroutine and uses it as the entry into the asyncio program.
The main() coroutine runs and creates a coroutine to run the background task in a new thread and then awaits this coroutine.
The main() coroutine suspends and the new coroutine runs, starts a thread, and executes the blocking function in the new thread.
The blocking function reports the thread name and whether it is a daemon.
We can see that the thread has a name indicating that it is associated with asyncio, e.g. “asyncio_0” and that it is not a daemon thread, e.g. not a background thread.
Because the new thread is not a daemon thread it means that the thread would prevent the main thread from exiting.
1 2 |
task is running name=asyncio_0, daemon=False |
Further Reading
This section provides additional resources that you may find helpful.
Python Asyncio Books
- Python Asyncio Mastery, Jason Brownlee (my book!)
- Python Asyncio Jump-Start, Jason Brownlee.
- Python Asyncio Interview Questions, Jason Brownlee.
- Asyncio Module API Cheat Sheet
I also recommend the following books:
- Python Concurrency with asyncio, Matthew Fowler, 2022.
- Using Asyncio in Python, Caleb Hattingh, 2020.
- asyncio Recipes, Mohamed Mustapha Tahrioui, 2019.
Guides
APIs
- asyncio — Asynchronous I/O
- Asyncio Coroutines and Tasks
- Asyncio Streams
- Asyncio Subprocesses
- Asyncio Queues
- Asyncio Synchronization Primitives
References
Takeaways
You now know how to execute blocking functions in new threads separate from the asyncio event loop.
Do you have any questions?
Ask your questions in the comments below and I will do my best to answer.
Photo by Grahame Jenkins on Unsplash
Do you have any questions?