# `asyncio` in Python

`asyncio` is Python's built-in package for writing asynchronous programs.
It is useful when a program spends time waiting for input/output, such as:

- waiting for a network response
- waiting for a file or subprocess
- waiting for a database
- waiting for messages from another process

The key idea is simple: while one task is waiting, Python can let another task
make progress.

## The Idea

In normal synchronous code, a function waits in place:

```text
One thread of execution

time -------------------------------------------------------------------->

Task A:  start -> send request -> WAIT...WAIT...WAIT -> response -> finish
                                                                      |
                                                                      v
Task B:                                                            start -> finish

Task B starts only after Task A is completely done.
```

The program is not broken; it is just blocked. It cannot move on to Task B
until Task A gets its response and finishes.

With `asyncio`, tasks cooperate through an event loop:

```text
                  Event loop
              +----------------+
              | what is ready? |
              +--------+-------+
                       |
        +--------------+--------------+
        |              |              |
        v              v              v
   Task A runs    Task B runs    Task C runs
        |              |              |
        v              v              v
   await I/O      await I/O      await I/O
        |              |              |
        +------ back to event loop ---+
```

When a task reaches `await`, it says:

> I am waiting for something slow. Let another task run until my result is ready.

## Main Pieces

`async def` defines a coroutine function. A coroutine is a function that can
pause at `await` points and resume later.

```python
async def fetch_data():
    ...
```

`await` pauses the current coroutine until an asynchronous operation finishes.
While it is paused, the event loop can run other coroutines.

```python
result = await some_async_operation()
```

`asyncio.run(...)` starts the event loop and runs one top-level coroutine until
it finishes.

```python
asyncio.run(fetch_data())
```

## Smallest Useful Example

This example uses `asyncio.sleep(...)` to pretend that each task is waiting for
something slow, like a network response.

You can run the same example from:

```bash
python docs/notes/asyncio/asyncio_example.py
```

```python
import asyncio


async def make_toast():
    print("Toast: start")
    await asyncio.sleep(2)
    print("Toast: done")


async def make_coffee():
    print("Coffee: start")
    await asyncio.sleep(1)
    print("Coffee: done")


async def main():
    await asyncio.gather(make_toast(), make_coffee())


asyncio.run(main())
```

Possible output:

```text
Toast: start
Coffee: start
Coffee: done
Toast: done
```

What is happening:

1. `asyncio.run(main())` starts the event loop.
2. `asyncio.gather(...)` starts both coroutines.
3. `make_toast()` reaches `await asyncio.sleep(2)` and pauses.
4. While toast is paused, `make_coffee()` gets a turn.
5. Coffee finishes first because it only waits 1 second.
6. Toast resumes when its 2-second wait is done.

The important mechanism is the `await`: it gives control back to the event loop
while the current coroutine is waiting.

## What It Is Not

`asyncio` is not the same as making Python run many CPU-heavy calculations at
the exact same time. It shines when the program is mostly waiting on I/O.

For CPU-heavy work, use tools like multiprocessing, native extensions, or
specialized libraries.

## Why It Shows Up In MCP

MCP clients often send a request to a server and then wait for a response.
That waiting is I/O, so the client side is naturally asynchronous.

In `src/mcp_simple_example.py`, the demo uses:

```python
asyncio.run(run_client_demo())
```

That means:

1. start Python's event loop
2. run the async MCP client demo
3. pause at `await` points while waiting for the MCP server
4. resume when replies arrive

The result is still readable Python, but it can handle waiting without freezing
the whole flow.
