Skip to content

Week 12 - Async & Await

AsyncIO in Python

Definition

The asyncio module is a library for writing concurrent code using the async/await syntax, enabling single-threaded, cooperative multitasking.

Use Case

Primarily used for I/O-bound operations (network requests, file reads/writes) where tasks spend a significant amount of time waiting for external resources.

When to Use

When you have many tasks that involve waiting, and you want to improve responsiveness and efficiency without the overhead of threads or processes.

Why it’s Better (than threads for I/O bound tasks)

  • Reduced Overhead: Avoids the context switching and memory overhead associated with threads, leading to better performance for I/O-bound tasks.
  • Predictable Execution: Cooperative multitasking allows for more control over task execution, reducing race conditions and deadlocks that can occur with threads.
  • Simplified Concurrency: async/await simplifies the syntax and logic of concurrent programming compared to traditional thread management.

Explicate a Code Example

1
2
3
4
# Sample async await block, websockets is a Python module for handling web communication.

async with websockets.serve(view_log, host, port):
    await asyncio.Future()

Explanation of example:

  • async with websockets.serve(view_log, host, port)::

    • Establishes a WebSocket server.
    • view_log is the function handling incoming WebSocket connections.
    • host and port define the server’s address.
    • async with ensures the server cleanly shuts down when exiting the block.
  • await asyncio.Future()::

    • Creates a Future object, representing a value that will become available later.
    • await suspends execution until the Future is resolved (which never happens in this case).
    • This effectively keeps the server running indefinitely, as the server will not exit this line of code.

This code starts a WebSocket server and then enters an infinite wait, keeping the server alive. In the context of Python’s asyncio and Future objects, resolving a Future means setting the result or exception associated with that Future.

  • Future Object:

    • A Future is a placeholder for a result that will become available at some point in the future.
    • It represents the eventual outcome of an asynchronous operation.
  • Resolving a Future:

    • Setting a Result: When the asynchronous operation completes successfully, the result is set using the set_result() method of the Future object. This signals that the value is now available.
    • Setting an Exception: If the asynchronous operation encounters an error, an exception is set using the set_exception() method. This indicates that the operation failed.
    • Once a Future is resolved, it cannot be resolved again.
    • When a Future is awaited, the await expression will return the result of the future, or raise the exception that was set.
  • Why it Matters:

    • await expressions suspend execution until a Future is resolved.
    • Resolving a Future triggers the resumption of the coroutine that was waiting for it.
    • This mechanism allows asynchronous tasks to communicate and synchronize their execution.

The Magic Behind await and Future of the asyncio Event Loop

  1. Coroutine Suspension:

    • When a coroutine encounters an await expression, it doesn’t just stop. Instead, it pauses its execution and yields control back to the event loop.
    • This pausing involves saving the coroutine’s current state:
      • The current execution frame (local variables, etc.).
      • The instruction pointer (the exact location in the code where execution should resume).
    • This state is stored internally by the coroutine object itself.
  2. Future and Callbacks:

    • The await expression typically involves a Future (or something that acts like one).
    • When a coroutine awaits a Future, it registers a callback function with that Future. This callback is essentially “run this coroutine when the Future is resolved.”
    • This callback is a special function that knows how to resume the waiting coroutine.
    • The Future object stores a list of these callbacks.
  3. Event Loop Coordination:

    • The event loop is the heart of asyncio. It manages the execution of coroutines and I/O operations.
    • When an I/O operation completes (e.g., data arrives from a network socket), the event loop is notified.
    • The event loop then checks if any Future objects are associated with the completed operation.
    • When the Future is resolved, the loop then calls all of the callbacks that are stored within the future.
    • The callback then tells the event loop to schedule the coroutine to continue running.
  4. Coroutine Resumption:

    • When the Future is resolved, the registered callback is executed.
    • This callback tells the event loop to resume the coroutine.
    • The event loop retrieves the saved state of the coroutine and resumes its execution from the exact point where it was suspended (the await expression).
    • This resumption is done by the event loop calling the coroutine’s .send() method, which sends the result of the future into the coroutine. If an exception was set, then the .throw() method is called on the coroutine.

The Future acts as a communication channel between the asynchronous operation and the waiting coroutine. The event loop acts as the orchestrator, managing the suspension and resumption of coroutines based on the state of Future objects. Essentially you have an Observer of Obersers pattern where the Future is the Subject of the Coroutine Observers while simultaneously being an Observer of the asyncio event loop.

Sequence Diagram of await and Future

sequenceDiagram participant Coroutine participant Future participant EventLoop Coroutine->>Future: await Future activate Coroutine Coroutine->>Future: Register Callback (resume coroutine) Coroutine->>EventLoop: Yield Control (suspend) deactivate Coroutine EventLoop->>ExternalIO: Perform I/O Operation ExternalIO-->>EventLoop: I/O Complete EventLoop->>Future: set_result() or set_exception() Future->>EventLoop: Notify Callbacks EventLoop->>Coroutine: Resume Coroutine (send/throw) activate Coroutine Coroutine-->>Future: Result/Exception received Coroutine->>EventLoop: Yield control or Continue deactivate Coroutine