Asyncio Run Multiple Concurrent Event Loops

April 30, 2024 Python Asyncio

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:

  1. 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.
  2. 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.
  3. 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?

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:

...
# 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:

...
# 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.

# 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.

...
# 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.

...
# start a new event loop
asyncio.run(main_coroutine('Loop2'))

Tying this together, the complete example is listed below.

# 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.

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.

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.

# 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.

...
# 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.

# 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.

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

Takeaways

You now know how to run multiple asyncio event loops concurrently.



If you enjoyed this tutorial, you will love my book: Python Asyncio Jump-Start. It covers everything you need to master the topic with hands-on examples and clear explanations.