Asyncio & Event loop

Awaitable

  • There are 3 types awaitable objects

  1. Coroutines

  2. Tasks - asyncio.Task

  3. Futures - asyncio.Future

Coroutine

  • A coroutine is a specialized function in Python used for asynchronous programming.

  • It allows you to pause and resume its execution at certain points, making it ideal for handling tasks that involve waiting (e.g., for I/O operations, timers, or other asynchronous events).

  • Coroutines work in conjunction with an event loop, which schedules and manages their execution.

async def my_coroutine():
    print("Start")
    await asyncio.sleep(1)  # Pause execution here
    print("Resume after 1 second")

How Coroutines Work

  1. Pausing Execution: When a coroutine encounters an await expression, it pauses its execution and hands control back to the event loop.

  2. Resuming Execution: The coroutine is resumed when the awaited task (e.g., asyncio.sleep(1)) is completed, allowing other tasks to run in the meantime.

Task

  • You can create separate tasks that run independently, allowing your program to continue without waiting for one task to finish.

  • Here, task1() and task2() run concurrently, and the program doesn't block while waiting for one task to complete.

import asyncio

async def task1():
    print("Task 1 started")
    await asyncio.sleep(2)  # Pause Task 1
    print("Task 1 completed")

async def task2():
    print("Task 2 started")
    await asyncio.sleep(1)  # Pause Task 2
    print("Task 2 completed")

async def main():
    # Create both tasks
    t1 = asyncio.create_task(task1())
    t2 = asyncio.create_task(task2())
    
    # Wait for both to complete
    await t1
    await t2

asyncio.run(main())
/** 
    Task 1 started
    Task 2 started
    Task 2 completed
    Task 1 completed
**/
  • asyncio.gather lets you wait for multiple tasks at the same time without blocking others. It collects results from all the coroutines when they're done.

import asyncio

async def task1():
    await asyncio.sleep(5)
    print("Task 1 done")

async def task2():
    print("Task 2 running")
    await asyncio.sleep(2)
    print("Task 2 done")

async def main():
    results = await asyncio.gather(task1(), task2())
    print("All tasks completed:", results)

asyncio.run(main())

Event Loop

  • Event loops use cooperative scheduling: an event loop runs one Task at a time. While a Task awaits for the completion of a Future, the event loop runs other Tasks, callbacks, or performs IO operations

  • When you call asyncio.run(main()), it creates a new event loop, runs the main() coroutine inside that loop, and waits for the coroutine to finish. It ensures the main() coroutine executes to completion.

  • How the Event Loop Handles Tasks

    1. Concurrency: The event loop enables tasks to run concurrently, meaning it can switch between tasks when one is waiting (e.g., during await asyncio.sleep() or an I/O operation). However, tasks do not actually run in parallel unless you explicitly use threads or processes.

    2. Single-threaded Nature: The event loop operates in a single thread. It executes one task at a time but quickly switches between them based on when they are ready to progress (non-blocking behavior).

    3. Task Switching:

      • When a task reaches an await point (e.g., waiting for a network response or a timer), it yields control back to the event loop.

      • The event loop then checks its queue of pending tasks and runs the next task that is ready to continue.

      • This gives the appearance of "simultaneous" execution but is actually task scheduling.

Vs Nodejs

Similarities

  1. Asynchronous Programming: Both Python's asyncio and Node.js's event loop enable non-blocking, asynchronous operations. This makes them ideal for I/O-bound tasks like handling multiple network requests.

  2. Single-threaded: Both event loops run on a single thread, processing tasks in an order determined by the scheduler.

  3. Callbacks and Promises: Node.js primarily uses callbacks and promises for asynchronous handling, while Python uses async/await, which is conceptually similar to promises.

Differences

  1. Native Integration:

    • Node.js has its event loop implemented directly in its runtime, based on libuv, a C library. The event loop is deeply integrated with Node's non-blocking I/O functions.

    • Python's asyncio is a library/module implemented on top of Python's runtime, rather than being a part of the core runtime.

  2. Task Scheduling:

    • In Node.js, the event loop has phases (e.g., timers, I/O callbacks, idle/prepare, poll, check, and close callbacks). It cycles through these phases repeatedly in the order defined by libuv.

    • In Python's asyncio, tasks are managed within an asyncio event loop using coroutines. Python doesn't have phases like Node.js but schedules tasks and runs them when their I/O operations complete.

  3. Concurrency:

    • Node.js is inherently single-threaded, but it uses its thread pool (via libuv) to offload heavy operations like file system tasks or cryptographic operations.

    • Python's asyncio can use await and coroutines for asynchronous tasks, and for true concurrency, Python can leverage threading or multiprocessing in addition to the asyncio event loop.

  4. API Design:

    • Node.js APIs heavily use callbacks and promise-based APIs, making it more callback-centric.

    • Python's asyncio uses coroutines with async def and await, which are often considered more readable.

References

Last updated

Was this helpful?