# Asyncio & Event loop

## Awaitable

* There are 3 types awaitable objects

1. Coroutines
2. Tasks - [asyncio.Task](https://docs.python.org/3/library/asyncio-task.html#asyncio.Task)
3. Futures - [asyncio.Future](https://docs.python.org/3/library/asyncio-future.html#asyncio.Future)

## Coroutine

* A coroutine is a specialized function in Python used for asynchronous programming.&#x20;
* 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).&#x20;
* Coroutines work in conjunction with an event loop, which schedules and manages their execution.

```python
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.

```python
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.

```python
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

{% embed url="https://myapollo.com.tw/blog/begin-to-asyncio/" %}

{% embed url="https://bbc.github.io/cloudfit-public-docs/asyncio/asyncio-part-2" %}
