44  Async Programming (Intro)

Modern versions of Python have support for “asynchronous code” using something called “coroutines”, with async and await syntax.

Asynchronous programming allows your program to work on multiple tasks without blocking. Think of it like a restaurant kitchen - instead of waiting for one dish to cook completely before starting another, the chef starts multiple dishes and tends to them as needed.

44.1 Key Concepts

44.1.1 Coroutines

A coroutine is a special function defined with async def. It can pause and resume execution.

import asyncio

async def fetch_data():
    """Simulate fetching data from API"""
    print("Starting to fetch...")
    await asyncio.sleep(2)  # Simulates I/O operation
    print("Data fetched!")
    return {"data": "example"}
fetch_data()
<coroutine object fetch_data at 0x107d12f80>

44.1.2 The Event Loop

The event loop is the core of async programming - it manages and executes coroutines.

┌─────────────────────────────────────┐
│          EVENT LOOP                 │
│                                     │
│  ┌─────────┐  ┌─────────┐           │
│  │ Task 1  │  │ Task 2  │  ...      │
│  └────┬────┘  └────┬────┘           │
│       │            │                │
│       ▼            ▼                │
│   [Running]    [Waiting]            │
│                                     │
└─────────────────────────────────────┘

44.2 Basic Example

import asyncio

async def say_hello(name, delay):
    """Greet after delay"""
    await asyncio.sleep(delay)
    print(f"Hello, {name}!")
Hello, Bob!
Hello, Alice!
Hello, Charlie!
# Concurrent
async def main():
    """Run multiple greetings concurrently"""
    # Create tasks
    task1 = asyncio.create_task(say_hello("Alice", 2))
    task2 = asyncio.create_task(say_hello("Bob", 1))
    task3 = asyncio.create_task(say_hello("Charlie", 3))
    
    # Wait for all tasks concurrently
    await asyncio.gather(task1, task2, task3)

# Run the async program
await main()
Hello, Bob!
Hello, Alice!
Hello, Charlie!
Time →
0s      1s      2s      3s
│       │       │       │
├───────┼───────┼───────┤
│                       │
Task1: [====await====][done]
│                       │
Task2: [=await=][done]  │
│                       │
Task3: [======await======][done]
        ↑       ↑       ↑
        │       │       │
     Bob says  Alice  Charlie
      hello    says    says
               hello   hello

44.3 Example: Fetching Multiple URLs

import asyncio
import aiohttp
import time

async def fetch_url(session, url):
    """Fetch a single URL"""
    async with session.get(url) as response:
        return await response.text()
# Sequential
async def fetch_all_sync_style():
    """Fetch URLs one by one (slow)"""
    urls = [
        "https://api.github.com/users/python",
        "https://api.github.com/users/github",
        "https://api.github.com/users/torvalds"
    ]
    
    async with aiohttp.ClientSession() as session:
        for url in urls:
            data = await fetch_url(session, url)
            print(f"Fetched {len(data)} bytes from {url}")
# Concurrent
async def fetch_all_async_style():
    """Fetch URLs concurrently (fast)"""
    urls = [
        "https://api.github.com/users/python",
        "https://api.github.com/users/github",
        "https://api.github.com/users/torvalds"
    ]
    
    async with aiohttp.ClientSession() as session:
        tasks = [fetch_url(session, url) for url in urls]
        results = await asyncio.gather(*tasks)
        
        for url, data in zip(urls, results):
            print(f"Fetched {len(data)} bytes from {url}")
# Compare performance
async def main():
    print("Synchronous style:")
    start = time.time()
    await fetch_all_sync_style()
    print(f"Time: {time.time() - start:.2f}s\n")
    
    print("Asynchronous style:")
    start = time.time()
    await fetch_all_async_style()
    print(f"Time: {time.time() - start:.2f}s")

await main()
Synchronous style:
Fetched 1263 bytes from https://api.github.com/users/python
Fetched 1241 bytes from https://api.github.com/users/github
Fetched 1223 bytes from https://api.github.com/users/torvalds
Time: 1.02s

Asynchronous style:
Fetched 1263 bytes from https://api.github.com/users/python
Fetched 1241 bytes from https://api.github.com/users/github
Fetched 1223 bytes from https://api.github.com/users/torvalds
Time: 0.12s

44.4 Common Patterns

44.4.1 Using asyncio.gather()

async def task_a():
    """First task"""
    await asyncio.sleep(1)
    return "Result A"

async def task_b():
    """Second task"""
    await asyncio.sleep(2)
    return "Result B"

async def main():
    """Run tasks concurrently"""
    results = await asyncio.gather(task_a(), task_b())
    print(results)  # ['Result A', 'Result B']
start: float = time.time()
await main()
print(f"Time: {time.time() - start:.2f}s")
['Result A', 'Result B']
Time: 2.00s

44.4.2 Using asyncio.create_task()

async def background_task():
    """Long running task"""
    while True:
        print("Background work...")
        await asyncio.sleep(5)

async def main():
    """Main with background task"""
    # Start background task
    task = asyncio.create_task(background_task())
    
    # Do other work
    await asyncio.sleep(10)
    
    # Cancel background task
    task.cancel()
start: float = time.time()
await main()
print(f"Time: {time.time() - start:.2f}s")
Background work...
Background work...
Time: 10.00s

44.4.3 Async Context Managers

class AsyncResource:
    """Example async context manager"""
    async def __aenter__(self):
        print("Acquiring resource...")
        await asyncio.sleep(1)
        return self
    
    async def __aexit__(self, exc_type, exc_val, exc_tb):
        print("Releasing resource...")
        await asyncio.sleep(0.5)

async def main():
    """Use async context manager"""
    async with AsyncResource() as resource:
        print("Using resource")
start: float = time.time()
await main()
print(f"Time: {time.time() - start:.2f}s")
Acquiring resource...
Using resource
Releasing resource...
Time: 1.50s

44.5 Concurrent vs Parallelism

  • Concurrent (Async/Await) -> I/O-bound operation

  • Parallelism -> CPU-bound operation

Feature Synchronous Asynchronous
Definition Executes one task at a time Can handle multiple tasks concurrently
Syntax def function(): async def function():
Calling result = function() result = await function()
Blocking Blocks until complete Non-blocking, can switch tasks
Best for CPU-bound tasks I/O-bound tasks
Complexity Simpler to understand More complex flow
Performance Slower for I/O tasks Faster for concurrent I/O