python LogoAsynchronous Tasks + asyncio

Asynchronous tasks in Python, primarily facilitated by the `asyncio` library, represent a paradigm for writing concurrent code without using threads or processes. Instead, `asyncio` employs a single-threaded, cooperative multitasking model based on an event loop. This approach is particularly effective for I/O-bound operations (like network requests, disk access, or database queries) where a program often spends a lot of time waiting for external resources.

Key Concepts:

1. Coroutines (`async def`): These are special functions defined with `async def`. When called, they don't immediately execute; instead, they return a coroutine object. Coroutines can be paused during their execution and resumed later.
2. `await`: This keyword can only be used inside an `async def` function. It's used to pause the execution of the current coroutine until the awaited object (another coroutine, a Future, or a Task) completes. While the current coroutine is paused, the `asyncio` event loop can switch control to other scheduled tasks, making efficient use of the waiting time.
3. Event Loop: The central component of `asyncio`. It manages and coordinates the execution of coroutines. It continuously monitors registered tasks, switches between them when one awaits an operation, and dispatches events.
4. Tasks (`asyncio.create_task()`): While `await` can directly execute a coroutine and wait for its result, `asyncio.create_task()` is used to schedule a coroutine to run concurrently on the event loop as an `asyncio.Task`. Tasks are essentially wrappers around coroutines that allow them to be scheduled, run in the background, and have their state managed by the event loop.
5. `asyncio.run()`: This is the high-level entry point for running an `asyncio` event loop. It takes a coroutine, runs it until it completes, and then closes the loop. It handles the lifecycle of the event loop automatically.
6. `asyncio.gather()`: Used to run multiple awaitable objects concurrently and wait for all of them to complete. It collects the results in the order the awaitables were provided.

How it Works:
When an `asyncio` program starts, an event loop is initiated. Coroutines are then registered with this loop (either directly via `asyncio.run()` or by creating `Tasks`). When a coroutine encounters an `await` expression, it yields control back to the event loop. The event loop then checks if there are other pending tasks that are ready to run. If so, it switches to one of them. Once the awaited operation completes (e.g., a network request returns data), the event loop notifies the original coroutine, and it can resume its execution from where it left off. This cooperative multitasking allows a single thread to handle many I/O operations efficiently, as it doesn't block while waiting.

Example Code

import asyncio
import time

async def perform_task(task_id, delay):
    """
    A simple asynchronous task that simulates work and waiting.
    """
    print(f"Task {task_id}: Starting (will take {delay} seconds)")
    await asyncio.sleep(delay)  Simulate an I/O-bound operation or delay
    print(f"Task {task_id}: Finished after {delay} seconds")
    return f"Result from Task {task_id}"

async def main():
    """
    The main asynchronous function to orchestrate multiple tasks.
    """
    start_time = time.monotonic()
    print("Main: Starting multiple asynchronous tasks...")

     Create tasks to run concurrently
    task1 = asyncio.create_task(perform_task(1, 3))  Task 1 will take 3 seconds
    task2 = asyncio.create_task(perform_task(2, 1))  Task 2 will take 1 second
    task3 = asyncio.create_task(perform_task(3, 2))  Task 3 will take 2 seconds

     Await all tasks to complete and gather their results.
     asyncio.gather waits for all provided awaitables (tasks in this case)
     to complete.
    results = await asyncio.gather(task1, task2, task3)

    end_time = time.monotonic()
    print(f"Main: All tasks completed. Total elapsed time: {end_time - start_time:.2f} seconds")
    print(f"Main: Task results: {results}")

if __name__ == "__main__":
     Run the main asynchronous function
    asyncio.run(main())