Python Programming from First Principles: Learn to Code, Think Computationally, and Solve Real-World Problems
PDFEPUB
BOOK 000 · CHAPTER 8 · Async I/O with asyncio

Async I/O with asyncio

On this pageHow Asyncio Works (Intuition First)Example: Running Multiple Asynchronous Tasks

When a program must handle massive numbers of small, I/O-heavy tasks, traditional threading begins to show its limits. Each thread carries memory overhead, and switching between thousands of them can slow a program down.

That’s where asyncio enters the picture.

asyncio is Python’s framework for asynchronous, event-driven programming. Instead of creating many threads, it runs all tasks inside one single thread, but lets those tasks cooperate by yielding control whenever they need to wait.

This model is perfect for programs that: - wait for network responses - handle thousands of client connections - coordinate APIs or microservices - manage background events or timers

The key ingredients are: - coroutines, defined with async def, which act like lightweight tasks - await, which pauses a coroutine so others can run - an event loop that schedules and coordinates all coroutines

Think of it as a well-organized single-lane road where every driver takes turns smoothly, rather than a chaotic multi-lane highway.

How Asyncio Works (Intuition First)

When a coroutine performs an operation that would normally block—like waiting for a network response—it does not freeze the entire program. Instead, it says: “I’m waiting; someone else can run now.”

await works with an awaitable operation such as asyncio.sleep. Calling a blocking synchronous function inside a coroutine would still block that event-loop thread.

While one coroutine pauses, another continues, and so on.

The event loop orchestrates all of this, switching between coroutines extremely quickly.

The result: true concurrency without threads, ideal for workloads that spend most of their time waiting.

Example: Running Multiple Asynchronous Tasks

Below is a simple but representative example.

It simulates several asynchronous “fetch” operations by using asyncio.sleep(), which behaves like a non-blocking network wait.

Before showing the code, note the structure: - async_fetch() is a coroutine that simulates an asynchronous task. - main() creates many such tasks and runs them together. - asyncio.gather() waits for all of them to finish. - asyncio.run() starts the event loop.

Save this program as async_demo.py and run it from a terminal. In a Jupyter notebook, an event loop is already running; replace the final asyncio.run(main()) with await main(). The list below holds coroutine objects; asyncio.gather schedules them and returns results in their original order.

Code Example

import asyncio
import random
import time

async def async_fetch(name):
    """Simulate asynchronous I/O with asyncio.sleep()."""
    delay = 0.1 + random.random() * 0.2
    await asyncio.sleep(delay)       # Non-blocking delay
    return f"OK {name} ({delay:.2f}s)"

async def main():
    # Launch multiple coroutines at the same time
    tasks = [async_fetch(f"task-{i}") for i in range(5)]
    start = time.perf_counter()

    # Run all tasks concurrently and collect their results
    results = await asyncio.gather(*tasks)

    for r in results:
        print(r)

    elapsed = time.perf_counter() - start
    print(f"Total time ~ {elapsed:.2f}s (async I/O)")

# Start the event loop
asyncio.run(main())
Listing 429 — Example: Running Multiple Asynchronous Tasks

What’s Happening?

  • All tasks run inside one thread

This example runs on one event-loop thread. Tasks can still interleave around await, so shared mutable state may require coordination.

  • Coroutines pause themselves with await

await asyncio.sleep() doesn’t block; it yields control back to the event loop.

  • Other coroutines run while one waits

Tasks take rapid turns, creating smooth, efficient concurrency.

  • The waits overlap

The example usually takes roughly the longest wait plus scheduling and printing overhead. Exact timings depend on the environment.

  • Designed for thousands of I/O-bound tasks

Coroutines are extremely lightweight, enabling large-scale systems like web servers, chat apps, and async API clients.