We can run multiple concurrent asyncio event loops by starting and running each new event loop in a separate thread.
Each thread can host and manage one event loop. This means we can start one thread per event loop we require, allowing a program to potentially scale from thousands to millions of coroutines.
In this tutorial, you will discover how to run multiple asyncio event loops concurrently.
Let’s get started.
Need Multiple Concurrent Asyncio Event Loops
There are situations where one asyncio event loop is not enough.
We may need to develop a program that runs multiple asyncio event loops concurrently.
Some common reasons include:
- Scalability: Running multiple asyncio event loops concurrently allows the program to scale better, especially in scenarios where there are many independent asynchronous tasks to be handled. By distributing the workload across multiple event loops, the program can utilize the available resources more efficiently and handle a higher volume of tasks concurrently.
- Resource Utilization: In applications with diverse asynchronous workloads, running multiple event loops can help maximize resource utilization. Different event loops can specialize in handling different types of tasks, such as I/O-bound or CPU-bound operations, and can be assigned to different processor cores. This can lead to better overall performance and responsiveness.
- Isolation and Modularity: Running multiple event loops provides a level of isolation and modularity within the program. Each event loop can be responsible for managing a specific set of tasks or components, making the codebase more modular and easier to understand, develop, and maintain. Additionally, running event loops concurrently can help prevent blocking operations in one part of the program from affecting the responsiveness of other parts.
Perhaps our application requires millions rather than thousands of concurrent tasks.
Perhaps our hardware has tens or hundreds of CPU cores, allowing for much greater scalability than a single event loop can support.
Perhaps the tasks running within one event loop can block or become unstable, requiring many concurrent event loops to overcome.
There are many cases where we may require more than asyncio event loop.
How can we run multiple asyncio event loops in Python?
Run loops using all CPUs, download your FREE book to learn how.
How to Run Multiple Asyncio Event Loops
We can run multiple asyncio event loops concurrently using threads.
Each Python thread is capable of running a single event loop. Therefore, we can start one new thread per event loop that we require.
This can be achieved by creating a new threading.Thread instance and specifying the asyncio.run() function that will start the event loop and the coroutine to execute.
We can then start the new thread which will in turn start the new event loop and execute our main coroutine.
For example:
1 2 3 4 5 |
... # define a new thread to run an asyncio event loop thread = threading.Thread(target=asyncio.run, args=(main(),)) # start the new thread with the new event loop thread.start() |
We can then start as many new threads with new event loops as we require.
For example:
1 2 3 4 5 6 7 8 9 |
... # define many threads to run event loops threads = [threading.Thread(target=asyncio.run, args=(main(),)) for _ in range(100)] # start all threads for thread in threads: thread.start() # wait for all threads to complete for thread in threads: thread.join() |
You can learn more about starting new threads in the tutorial:
You can learn about starting asyncio event loops in the tutorial:
You can learn more about how to run an event loop in a separate thread in the tutorial:
Now that we know how to run multiple asyncio event loops in separate threads, let’s look at some worked examples.
Example of Loop in Main Thread And New Thread
We can explore how to run two asyncio event loops concurrently, one in the main thread and one in a new separate thread.
In this case, we will define a simple main coroutine that reports a message, reports the details of the loops, suspends a moment, and then reports a final message.
The main coroutine takes a name argument that we can report in our message. Each main coroutine we create and run we can assign a different name, allowing us to tell the event loops apart.
1 2 3 4 5 6 7 8 9 10 11 12 |
# main coroutine for the asyncio program async def main_coroutine(name): # report a message print(f'{name} coroutine running...', flush=True) # get the loop for this thread loop = asyncio.get_running_loop() # report details of the current event loop print(f'{name}: {id(loop)} - {loop}', flush=True) # suspend a moment await asyncio.sleep(2) # report a final message print(f'{name} done', flush=True) |
We can then start a new thread to run this coroutine in a separate event loop.
1 2 3 4 5 |
... # create a new thread to execute a target coroutine thread = threading.Thread(target=asyncio.run, args=(main_coroutine('Loop1'),)) # start the new thread thread.start() |
We can then start another asyncio event loop in the main thread and run the same main coroutine with a different argument.
1 2 3 |
... # start a new event loop asyncio.run(main_coroutine('Loop2')) |
Tying this together, 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 two event loops concurrently import threading import asyncio import time # main coroutine for the asyncio program async def main_coroutine(name): # report a message print(f'{name} coroutine running...', flush=True) # get the loop for this thread loop = asyncio.get_running_loop() # report details of the current event loop print(f'{name}: {id(loop)} - {loop}', flush=True) # suspend a moment await asyncio.sleep(2) # report a final message print(f'{name} done', flush=True) # create a new thread to execute a target coroutine thread = threading.Thread(target=asyncio.run, args=(main_coroutine('Loop1'),)) # start the new thread thread.start() # start a new event loop asyncio.run(main_coroutine('Loop2')) # wait for the other thread to be done thread.join() |
Running the example first creates a new thread configured to run a new asyncio event loop with our main_coroutine() coroutine.
The new thread is then started. This immediately creates a new native thread that creates a new event loop and runs our main coroutine.
The main thread then starts another asyncio event loop and runs our main_coroutine(). The main_coroutine() runs, reports a message, reports the details of the event loop, and suspends.
The separate thread runs, reports a message, reports the details of its event loop, and also suspends.
The main coroutine in each event loop resumes, reports a final message, and terminates.
The separate thread terminates and the main thread terminates, closing the application.
This highlights how we can run two concurrent asyncio event loops, one in the main thread and one in a new separate thread.
1 2 3 4 5 6 |
Loop2 coroutine running... Loop2: 4464595088 - <_UnixSelectorEventLoop running=True closed=False debug=False> Loop1 coroutine running... Loop1: 4454466192 - <_UnixSelectorEventLoop running=True closed=False debug=False> Loop2 done Loop1 done |
Next, let’s look at how we might run many multiple concurrent event loops.
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 Separate Loop in Each Thread
We can explore how to run many concurrent asyncio event loops.
In this case, we will update the main_coroutine() coroutine from the previous section so that it runs a loop, reports a message, and suspends each iteration. We want to confirm that coroutines in each event loop are indeed running concurrently.
The updated main coroutine is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
# main coroutine for the asyncio program async def main_coroutine(name): # report a message print(f'{name} coroutine running...', flush=True) # get the loop for this thread loop = asyncio.get_running_loop() # report details of the current event loop print(f'{name}: {id(loop)} - {loop}', flush=True) for i in range(6): # report a message print(f'{name}: running...', flush=True) # suspend a moment await asyncio.sleep(0.5) # report a final message print(f'{name} done', flush=True) |
We don’t have to run the same main coroutine in each separate thread, it is just a simplification we can use in this example.
Next, in the main thread will define a list of 5 new threads, each configured to create and run a new asyncio event loop. Each new thread is then started and we will wait for all threads to be done.
The main thread does not run an event loop in this case, instead it orchestrates the event loops in separate threads.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
... # create and start the threads threads = list() for i in range(5): # create the thread thread = threading.Thread(target=asyncio.run, args=(main_coroutine(f'Loop{i}'),)) # store threads.append(thread) # start the thread thread.start() # wait for all threads to be done for thread in threads: thread.join() |
Tying this together, 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 one loop in each thread import threading import asyncio import time # main coroutine for the asyncio program async def main_coroutine(name): # report a message print(f'{name} coroutine running...', flush=True) # get the loop for this thread loop = asyncio.get_running_loop() # report details of the current event loop print(f'{name}: {id(loop)} - {loop}', flush=True) for i in range(6): # report a message print(f'{name}: running...', flush=True) # suspend a moment await asyncio.sleep(0.5) # report a final message print(f'{name} done', flush=True) # create and start the threads threads = list() for i in range(5): # create the thread thread = threading.Thread(target=asyncio.run, args=(main_coroutine(f'Loop{i}'),)) # store threads.append(thread) # start the thread thread.start() # wait for all threads to be done for thread in threads: thread.join() |
Running the example first defines a list of 5 new threads, each configured to start and run a new asyncio event loop with our main_coroutine() coroutine.
Each new thread is started and the main thread suspends, waiting for all threads to be done.
Each new thread runs and starts a new and separate event loop. The main coroutine runs in each event loop first reporting a message, then reporting the details of its event loop object.
Each event loop runs a loop that reports a message and suspends each iteration. The reported messages are interleaved showing that all 5 event loops are running concurrently.
The main coroutine reports a final message in each event loop and the event loops and their host thread terminate.
Finally, the main thread terminates and the program is closed.
This highlights how we can run an arbitrary number of asyncio event loops concurrently.
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 35 36 37 38 39 40 41 42 43 44 45 |
Loop0 coroutine running... Loop0: 4559115600 - <_UnixSelectorEventLoop running=True closed=False debug=False> Loop0: running... Loop1 coroutine running... Loop1: 4559280400 - <_UnixSelectorEventLoop running=True closed=False debug=False> Loop1: running... Loop2 coroutine running... Loop2: 4559282896 - <_UnixSelectorEventLoop running=True closed=False debug=False> Loop2: running... Loop3 coroutine running... Loop3: 4559284304 - <_UnixSelectorEventLoop running=True closed=False debug=False> Loop3: running... Loop4 coroutine running... Loop4: 4559286736 - <_UnixSelectorEventLoop running=True closed=False debug=False> Loop4: running... Loop1: running... Loop0: running... Loop3: running... Loop2: running... Loop4: running... Loop3: running... Loop1: running... Loop0: running... Loop4: running... Loop2: running... Loop1: running... Loop0: running... Loop2: running... Loop4: running... Loop3: running... Loop2: running... Loop4: running... Loop0: running... Loop3: running... Loop1: running... Loop2: running... Loop0: running... Loop1: running... Loop4: running... Loop3: running... Loop2 done Loop0 done Loop4 done Loop1 done Loop3 done |
Overwhelmed by the python concurrency APIs?
Find relief, download my FREE Python Concurrency Mind Maps
Takeaways
You now know how to run multiple asyncio event loops concurrently.
Did I make a mistake? See a typo?
I’m a simple humble human. Correct me, please!
Do you have any additional tips?
I’d love to hear about them!
Do you have any questions?
Ask your questions in the comments below and I will do my best to answer.
Photo by Diana Măceşanu on Unsplash
Do you have any questions?