Key concepts of asyncio

9 minutes read

Asyncio is a library in Python that facilitates concurrent code writing using the async and await syntax. It's designed for managing I/O-bound tasks, enabling developers to write more responsive applications. This topic will explore the key concepts of asyncio, including awaitables, futures, tasks, and yielding control.

Awaitables

In the context of asyncio, an awaitable is an object that can be used in an await expression. Awaitables are essential for asynchronous programming, allowing functions to pause and yield control to the event loop. Three main types of objects are considered awaitable in asyncio:

  1. Coroutines are defined with async def; coroutines are the most common type of awaitable. They can contain one or more await expressions, allowing them to pause and resume execution.

  2. Tasks schedule coroutines concurrently. When a coroutine is wrapped in a Task object, it can run parallel to other tasks and coroutines.

  3. Futures represent a future result. It's a low-level awaitable that provides a way to track the state of a background operation.

The following class hierarchy shows the relationships between them:

Awaitable hierarchy

Tasks

Tasks are fundamental in asyncio that enable the concurrent execution of coroutines. They are responsible for scheduling and running coroutines in the event loop, enabling more efficient use of system resources

A coroutine, defined using the async def syntax, is a function that can pause and resume, allowing other operations to run during these pauses.

async def hello_world():
    print("Hello, world!")

To execute the coroutine, use the await keyword or schedule it with asyncio.run() or asyncio.create_task().

import asyncio

async def hello_world():
    print("Hello, world!")

task = asyncio.create_task(hello_world())

Tasks are objects that wrap coroutines for execution. A coroutine becomes a task when it's passed to asyncio.create_task() that schedules it to run.

Futures

In asyncio, a future represents a computation that hasn't necessarily been completed yet. It's a low-level awaitable object that encapsulates an eventual result of an asynchronous operation.

When a future object is awaited, the coroutine will wait until the future is resolved somewhere else. This is particularly useful when you have an asynchronous operation that a non-async function will complete, and you must wait for the result.

Here's an example of a future:

import asyncio

async def main():
    future = asyncio.Future()

    # Schedule the setting of the future result
    asyncio.create_task(set_after(future, 1, '...world'))

    print('Hello...', end='')

    # Wait until the future is done
    print(await future)

async def set_after(future, delay, value):
    # Simulate a delay with asyncio.sleep
    await asyncio.sleep(delay)

    # Set the result of the future
    future.set_result(value)

asyncio.run(main())

In this example, asyncio.Future() creates a future object. The set_after coroutine is scheduled to run as a task with asyncio.create_task(), and it sets the result of the future after a delay. The main coroutine then waits for the Future to be done using await.

The ensure_future function in asyncio is another way to create a Task from a coroutine. It's similar to create_task, but can also accept a future and ensures that it is wrapped as a Task. This can be useful when working with coroutines and futures, as it allows for more flexibility in scheduling asynchronous operations.

Here's how you might use ensure_future:

task = asyncio.ensure_future(some_coroutine())

It returns a Task object, and you can use await to wait for its completion just like you would with a Future created using asyncio.create_task().

Exception handling

Handling exceptions is a fundamental part of programming. In asyncio, exceptions work similarly to regular Python code but with some special considerations due to the asynchronous execution.

When an exception occurs inside a coroutine, it propagates up to the point where the coroutine is awaited. We can use try/except blocks to handle these exceptions.

import asyncio

async def my_coroutine():
    try:
        await asyncio.sleep(1)
        # This will raise a ZeroDivisionError
        result = 1 / 0
    except ZeroDivisionError:
        print("Caught a ZeroDivisionError")

async def main():
    await my_coroutine()

asyncio.run(main())

In the code above, we are catching a ZeroDivisionError inside the my_coroutine coroutine.

If an exception is raised inside a task and is missed, the exception will be propagated where the task object is awaited. If the task is not awaited, the exception will be ignored until the task object is garbage collected. At this point, the Task.__del__ method is called, and the exception is logged as an unhandled error.

import asyncio

async def my_coroutine():
    await asyncio.sleep(1)
    # This will raise a ZeroDivisionError
    result = 1 / 0

async def main():
    task = asyncio.create_task(my_coroutine())
    try:
        await task
    except ZeroDivisionError:
        print("Caught a ZeroDivisionError")

asyncio.run(main())

In this case, we're creating a Task from the my_coroutine coroutine, and we're catching the ZeroDivisionError where the task is awaited, inside the main function.

In summary, handling exceptions in asyncio is similar to regular Python code, but remember how your Tasks and coroutines are structured. Unhandled exceptions can lead to hard-to-debug situations, so it's good practice to handle exceptions where you expect they might occur.

Conclusion

In this topic, we've explored key concepts of asyncio: tasks, futures, and exception handling. Understanding these are vital for working effectively with asyncio:

  • Tasks offer a way to schedule coroutines concurrently.
  • Futures represent a computation that hasn't been completed yet.
  • Proper exception handling ensures smooth and error-resilient asynchronous program execution.

These concepts provide the foundational knowledge to use asyncio effectively, enabling you to write more robust and efficient Python code. By mastering them, you're now better equipped to leverage the asyncio capabilities in your projects.

13 learners liked this piece of theory. 11 didn't like it. What about you?
Report a typo