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 |
|
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
andport
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 theFuture
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.
- Creates a
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.
- A
-
Resolving a Future:
- Setting a Result: When the asynchronous operation completes successfully, the result is set using the
set_result()
method of theFuture
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.
- Setting a Result: When the asynchronous operation completes successfully, the result is set using the
-
Why it Matters:
await
expressions suspend execution until aFuture
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¶
-
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.
- When a coroutine encounters an
-
Future and Callbacks:
- The
await
expression typically involves aFuture
(or something that acts like one). - When a coroutine
await
s aFuture
, it registers a callback function with thatFuture
. 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.
- The
-
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.
- The event loop is the heart of
-
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.
- When the
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.