Event loops are used to execute and generally manage asynchronous tasks. Any asynchronous program in Python must directly or indirectly interact with the event loop.

Some higher level asyncio functions such as asyncio.run() simplifies using event loops. For example when you execute an asynchronous task with asyncio.run(), you do not have to manually interact with the event loop, as the creation, usage, and closing of the loop is handled automatically in the background

Consider the following example:

import asyncio

#a coroutine
async def display_nums():
  for n in range(4):
    print(n)
    await asyncio.sleep(0.001)#simulate a small delay

#another coroutine
async def display_letters():
  for letter in 'abcd':
    print(letter)
    await asyncio.sleep(0.001)

async def main():
  print('Started')
  await asyncio.gather(display_nums(), display_letters())
  print("Finished")

asyncio.run(main())

In the above example, we have defined three coroutines; display_nums(), display_letters() and main(). We awaited display_nums() and display_letters() in main(), then used asyncio.run() function to execute main().

When you use asyncio.run() function to execute a coroutine as in above, you do not have to directly interact with the event loop as this is handled automatically. In the background, the asyncio.run() performs the following operations:

  1. Creates a new event loop.
  2. Sets the created event loop as the current event loop for the running thread.
  3. Executes the coroutine passed to it using the created event loop.
  4. Closes the event loop.

Note: Using the asyncio.run() function is usually sufficient for most practical cases. However, in some cases you may need to manually interact with the event loop, this is what the following sections will cover. 

Create or Get the event loop

There are several asyncio functions we can use to create or get the event loop.

Create a new event loop

To create a new loop, use the asyncio.new_event_loop() function.

import asyncio

loop = asyncio.new_event_loop()

#perform some checks.
print(loop.is_running())
loop.close()
print(loop.is_closed())

The loop.is_running() and loop.is_closed() methods can be used to get the state of the loop. The loop.close() method closes the event loop, a closed event loop cannot be used to execute tasks anymore.

Get thread's  current event loop

An event loop can be associated with thread.  For example, whenever a Python program starts, an event loop is created and assigned to the main thread. We can use and reuse  this event loop, instead of creating a new one.

To access the current loop for the running thread, we use the asyncio.get_event_loop() function.

import asyncio

loop = asyncio.get_event_loop()

#perform some checks.
print(loop.is_running())
print(loop.is_closed())

False
False

Note that calling the, get_event_loop() method will return the same loop with each call.

In some cases, there may be no event loop that is associated with the running thread, in such a case, Python may issue a warning or even raise an exception. You can use the set_event_loop() function to set the event loop for the current thread, so that this loop will be returned whenever the get_event_loop() function is called in the thread.

import asyncio

loop = asyncio.new_event_loop()

asyncio.set_event_loop(loop)

print(asyncio.get_event_loop() == loop)

True

Get the running event loop

Inside of a coroutine function, we can access the event loop in which the coroutine is being executed using the asyncio.get_running_loop() function. Consider the following example:

import asyncio

async def main():
  loop = asyncio.get_running_loop()
  print(loop.is_running()) #is the loop running?
  print(loop.is_closed()) #is the loop closed?

asyncio.run(main())

In the above examples, we have seen how we can create or access an event loop, let us now look at how to make the event loop do what they are made for.

How to Run tasks/coroutines in an event loop

The basic tool for executing a coroutine inside of an event loop is the loop.run_until_complete() method. As the name of the method suggests, the method runs a task/coroutine given as argument until it is fully executed.

import asyncio

#a coroutine
async def display_nums():
  for n in range(4):
    print(n)
    await asyncio.sleep(0.001)#simulate a small delay

loop = asyncio.new_event_loop()
loop.run_until_complete(display_nums())
loop.close()

In the above example, we have a single coroutine function, display_nums(), we created a new event loop then used the loop.run_until_complete() method to execute the coroutine.

run multiple coroutines

The primary goal of asynchronous programming is to execute multiple coroutines in a non-blocking manner. This means that if a delay is encountered in the running coroutine, the coroutine can be suspended to give way for another coroutine to be executed before resuming.  At the start,  we saw how we can achieve this with asyncio.run() function by gathering the coroutines inside of the another coroutine eg main.

To manually execute multiple tasks/coroutines using loop.run_until_complete(), we will need to follow the steps outlined below:

  1. Create/get the event loop.
  2. Schedule each coroutine for execution using the loop.create_task() method.
  3. Combine the scheduled coroutines into a single runable object using asyncio.gather() function.
  4. Call the loop.run_until_complete() method with the object obtained above in step 4.
import asyncio

#a coroutine
async def display_nums():
  for n in range(4):
    print(n)
    await asyncio.sleep(0.001)#simulate a small delay

#another coroutine
async def display_letters():
  for letter in 'abcd':
    print(letter)
    await asyncio.sleep(0.001)

loop = asyncio.new_event_loop()  #create an event loop
loop.create_task(display_nums()) #schedule display_nums()
loop.create_task(display_letters()) #schedule display_letters()

tasks = asyncio.all_tasks(loop) #get all scheduled tasks

group = asyncio.gather(*tasks) #Combine the tasks into a single object

loop.run_until_complete(group)
loop.close()