Intro to asyncio

What does it mean for something to be asynchronous?

It means that being able to do multiple things at once. The asyncio library in Python provides the framework to do this.

Consider a scenario where you have a long running task, which you’d like to perform multiple times. In a traditional synchronous programming, you’d execute one task after another. If one task is taking 10 seconds to complete, running the task 6 times will take 1 full minute.

Here’s a simple code to demonstrate that:

import time


def long_running_task(time_to_sleep: int) -> None:
    print(f"Begin sleep for {time_to_sleep}")
    time.sleep(time_to_sleep)
    print(f"Awake from {time_to_sleep}")


def main() -> None:
    long_running_task(2)
    long_running_task(10)
    long_running_task(5)


if __name__ == "__main__":
    s = time.perf_counter()
    main()
    elapsed = time.perf_counter() - s
    print(f"Execution time: {elapsed:0.2f} seconds.")

In the above example, we wanted to run long_running_task 3 times, each with varying time to complete.

When the code run, we can see the following output:

Begin sleep for 2
Awake from 2
Begin sleep for 10
Awake from 10
Begin sleep for 5
Awake from 5
Execution time: 17.01 seconds.

The above was an example of synchronous execution, the long_running_task was executed one at a time, and each time we’re waiting for it to complete before starting another one.

Now let’s take a look on how it would look like if we’re running it asynchronously using asyncio.

First we convert the long_running_task into a coroutine, by adding the keyword async:

import asyncio


async def long_running_task(time_to_sleep):
    print(f"Begin sleep for {time_to_sleep}")
    await asyncio.sleep(time_to_sleep)
    print(f"Awake from {time_to_sleep}")

Note that coroutine cannot simply be called like regular functions. For example, you can type the following:

>>> long_running_task()
<coroutine object task at 0x1016dff40>

But the code was not executed. It does not print out Begin sleep .. or Awake from.

To actually execute the coroutine, you have three options:

asyncio.run(long_running_task(3))
  • await-ing the coroutine

async def main():
    await long_running_task(3)

asyncio.run(main())
async def main():
    task = asyncio.create_task(long_running_task(3))

    await task

asyncio.run(main())

Suppose now we want to execute long_running_task three times asynchronously:

import asyncio
import time


async def long_running_task(time_to_sleep: int) -> None:
    print(f"Begin sleep for {time_to_sleep}")
    await asyncio.sleep(time_to_sleep)
    print(f"Awake from {time_to_sleep}")


async def main() -> None:
    task1 = asyncio.create_task(long_running_task(2))
    task2 = asyncio.create_task(long_running_task(10))
    task3 = asyncio.create_task(long_running_task(5))
    await asyncio.gather(task1, task2, task3)


if __name__ == "__main__":
    s = time.perf_counter()
    asyncio.run(main())
    elapsed = time.perf_counter() - s
    print(f"Execution time: {elapsed:0.2f} seconds.")

The output:

Begin sleep for 2
Begin sleep for 10
Begin sleep for 5
Awake from 2
Awake from 5
Awake from 10
Execution time: 10.01 seconds.

Notice how the tasks are all starting at about the same time, and that the third (5) task was completed before the second one (10) was finished. The total time taken was faster compared to when it was run synchronously.