> ## Documentation Index
> Fetch the complete documentation index at: https://python4ai.codewithsiva.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Asynchronous Programming

> Learn async/await, concurrency, and how Python handles high-performance I/O operations

## 1. Concurrency vs. Parallelism

To understand asynchronous programming, we must distinguish between concurrency and parallelism:

* **Concurrency**: Handling multiple tasks during overlapping periods of time. Tasks do not necessarily execute at the same time. **Concurrency means multiple tasks are in progress.**

  ```
  Task A ────────
  Task B    ────────
  Task C       ────────
  ```

* **Parallelism**: Executing multiple tasks at the exact same time, usually on different CPU cores (e.g., rendering video, matrix multiplication).

  ```
  Core 1 → Task A ███████
  Core 2 → Task B ███████
  ```

**Parallelism is a type of concurrency, but concurrent tasks do not have to execute simultaneously.**

## 2. I/O-Bound vs. CPU-Bound Tasks

Understanding the nature of your workload helps determine the best concurrency model.

#### I/O-Bound Tasks

The program spends most of its time **waiting** for external operations to complete.

* **Examples**: API requests, database queries, network requests, disk file operations.
* **Solution**:
  * If an asynchronous library is available: **Asyncio**
  * If only synchronous/blocking libraries are available: **Multithreading**

#### CPU-Bound Tasks

The program spends most of its time **calculating** and performing computations on the CPU.

* **Examples**: Large mathematical calculations, image/video processing, machine learning inference, cryptographic operations.
* **Solution**: **Multiprocessing**

## 3. Concurrency Models in Python

Python provides three primary ways to handle concurrent execution: Asyncio, Multithreading, and Multiprocessing.

#### A. Asyncio (Asynchronous I/O)

`asyncio` provides concurrency using an **event loop**, typically running in a single thread. When a task waits for I/O, it pauses and yields control back to the event loop.

```python theme={null}
import asyncio

async def task(name, seconds):
    print(f"{name} started")
    await asyncio.sleep(seconds)
    print(f"{name} completed")

async def main():
    # Runs A and B concurrently
    await asyncio.gather(
        task("A", 3),
        task("B", 2)
    )

asyncio.run(main())
```

```
One Thread
    ↓
Event Loop
    ├── Task A → await → pause
    ├── Task B → await → pause
    └── Run whichever task is ready
```

* **Best for**: Async API calls, async database queries, WebSockets, network operations, and handling many concurrent I/O operations.

> **Note:** Use `asyncio` when the I/O library supports asynchronous operations.

#### B. Multithreading

Multithreading uses multiple threads within a single process. Because of Python's Global Interpreter Lock (GIL), only one thread can execute Python bytecodes at a time, making threads ideal for waiting on blocking network or file calls rather than CPU calculations.

```python theme={null}
import threading
import time

def task(name):
    print(f"{name} started")
    time.sleep(3)
    print(f"{name} completed")

# Create threads
t1 = threading.Thread(target=task, args=("A",))
t2 = threading.Thread(target=task, args=("B",))

# Start threads
t1.start()
t2.start()

# Wait for threads to finish
t1.join()
t2.join()
```

* `start()` → Starts the thread's activity.

* `join()` → Blocks the calling thread until the thread whose `join()` method is called terminates.

* **Best for**: Blocking API calls, synchronous libraries, blocking file/network operations, and legacy code without async support.

> **Note:** Use threads when the operation is I/O-bound but the library is synchronous/blocking.

#### C. Multiprocessing

Multiprocessing creates separate processes, each with its own Python interpreter and memory space. This completely bypasses the GIL, allowing true parallel execution across multiple CPU cores.

```python theme={null}
from multiprocessing import Process

def task():
    print("Running")

if __name__ == "__main__":
    p1 = Process(target=task)
    p2 = Process(target=task)

    p1.start()
    p2.start()

    p1.join()
    p2.join()
```

```
Core 1 → Process 1
Core 2 → Process 2
```

* **Best for**: Heavy calculations, image/video processing, CPU-intensive data manipulation, and computational algorithms.

> **Note:** Use multiprocessing when the work requires significant CPU computation.

## 4. Async & Await Declarations

Python's `asyncio` framework uses the keywords `async def` and `await` to write asynchronous code.

#### Coroutines

Declaring a function with `async def` creates a **coroutine**. Calling a coroutine does not run it; it returns a coroutine object. To execute it, you must `await` it.

```python theme={null}
import asyncio

async def fetch_data():
    print("Start fetching data...")
    # Simulate a network delay (non-blocking sleep)
    await asyncio.sleep(2)
    print("Data fetched!")
    return {"data": 123}

async def main():
    # We must await the coroutine to run it
    result = await fetch_data()
    print(result)

# Runs the event loop and executes the main coroutine
asyncio.run(main())
```

## 5. The Event Loop & Task Scheduling

The **Event Loop** is the engine that runs asynchronous applications. It manages the execution of different tasks:

1. It runs a task until the task hits an `await` expression (blocking I/O).
2. While that task waits for I/O (e.g., waiting for database results), the loop pauses it and switches to run another ready task.
3. Once the I/O operation finishes, the event loop resumes the original task.

#### Running Tasks Concurrently

To run multiple operations concurrently instead of sequentially, you can group them into `asyncio.create_task()` or use `asyncio.gather()`.

```python theme={null}
import asyncio
import time

async def call_api(service_name: str, delay: int):
    print(f"Calling {service_name}...")
    await asyncio.sleep(delay)
    print(f"{service_name} done!")
    return f"{service_name} response"

async def main():
    start_time = time.time()
    
    # Run three API calls concurrently
    results = await asyncio.gather(
        call_api("Auth Service", 2),
        call_api("Product Catalog", 1),
        call_api("Payment Gateway", 3)
    )
    
    end_time = time.time()
    print(f"Results: {results}")
    print(f"Total elapsed time: {end_time - start_time:.2f} seconds")  # Should be ~3 seconds instead of 6

asyncio.run(main())
```

## 6. Why FastAPI Uses Async

FastAPI is built on **ASGI** (Asynchronous Server Gateway Interface) and supports native `async def` route handlers.

When a client sends a request to an `async def` endpoint that performs a database query or external API fetch, FastAPI yields control back to the event loop. The event loop can process other incoming requests in the meantime, resulting in massive throughput gains.

## 7. Quick Decision Guide

Use this flowchart and table to quickly decide which concurrency model fits your task:

```
       What type of task?
              │
       ┌──────┴──────┐
       │             │
   I/O-bound   CPU-bound
   │             │
   ├──────┐      ↓
   │      │   Multiprocessing
   Async  Sync
   │      │
   ↓      ↓
   asyncio  Multithreading
```

| Task                      | Recommended Model |
| ------------------------- | ----------------- |
| Async API calls           | `asyncio`         |
| Async DB queries          | `asyncio`         |
| WebSockets                | `asyncio`         |
| Many network requests     | `asyncio`         |
| Blocking `requests` calls | Multithreading    |
| Synchronous library       | Multithreading    |
| Blocking I/O              | Multithreading    |
| Heavy calculations        | Multiprocessing   |
| Image/video processing    | Multiprocessing   |
| CPU-intensive work        | Multiprocessing   |

#### Final Summary

* **Asyncio** → Asynchronous I/O
* **Multithreading** → Blocking/Synchronous I/O
* **Multiprocessing** → CPU-bound work
* **Concurrency** = Multiple tasks are in progress (overlapping execution).
* **Parallelism** = Multiple tasks execute simultaneously (requires multiple CPU cores).

> **Key Takeaway:** Parallelism is a form of concurrency, but concurrency does not necessarily mean parallelism.

## Practice & Exercises

To reinforce what you've learned in this section (async/await declarations, tasks, event loops, and concurrency), practice with these interactive notebooks:

<CardGroup cols={2}>
  <Card title="Follow-Along Practice" icon="laptop-code">
    Practice defining async coroutines, working with non-blocking sleeps, understanding task scheduling, and implementing concurrent operations using asyncio.gather.

    [💻 VS Code](vscode://file/Users/sivaprasad/Downloads/python%20material/fastapi-course/public/notebooks/basics_exercises/Async_Programming_Practice.ipynb) | [🚀 Colab](https://colab.research.google.com/github/prasad230776/fastapi-course/blob/master/public/notebooks/basics_exercises/Async_Programming_Practice.ipynb) | <a href="/public/notebooks/basics_exercises/Async_Programming_Practice.ipynb" download>📥 Download</a>
  </Card>

  <Card title="Practice Exercises" icon="pen-to-square">
    Test your knowledge with hands-on exercises including simulated async file downloaders and concurrent batch file managers.

    [💻 VS Code](vscode://file/Users/sivaprasad/Downloads/python%20material/fastapi-course/public/notebooks/basics_exercises/Async_Programming_Exercises.ipynb) | [🚀 Colab](https://colab.research.google.com/github/prasad230776/fastapi-course/blob/master/public/notebooks/basics_exercises/Async_Programming_Exercises.ipynb) | <a href="/public/notebooks/basics_exercises/Async_Programming_Exercises.ipynb" download>📥 Download</a>
  </Card>
</CardGroup>
