async_patterns.py

script

← Back to skill

Content hash: 904354a6d57eca65faf3e40ff729e09809b809f5a90ee802a5724aa8e2c71369
#!/usr/bin/env python3
"""Idiomatic asyncio patterns: gather, tasks, timeouts, locks, and to_thread."""

from __future__ import annotations

import asyncio
import time
from typing import Any


# - Safe concurrent I/O with gather -


async def fetch(url: str) -> str:
    """Mock I/O-bound fetch."""
    await asyncio.sleep(0.1)
    return f"<content from {url}>"


async def gather_demo() -> list[str]:
    """Run multiple I/O calls concurrently - total time ~ slowest, not sum."""
    results = await asyncio.gather(
        fetch("https://a.example"),
        fetch("https://b.example"),
        fetch("https://c.example"),
    )
    return list(results)


# - Tasks (keep references!) -


async def task_demo() -> None:
    """Create tasks for background work - MUST keep a reference."""
    tasks: list[asyncio.Task[Any]] = []
    for i in range(3):
        t = asyncio.create_task(fetch(f"task-{i}"), name=f"fetch-{i}")
        tasks.append(t)  # critical: prevent GC

    done, pending = await asyncio.wait(tasks, timeout=5.0)
    for task in pending:
        task.cancel()
    for task in done:
        print(f"  {task.get_name()}: {task.result()}")


# - Timeouts -


async def timeout_demo() -> str | None:
    """Bound an async operation with a deadline."""
    try:
        async with asyncio.timeout(0.05):  # short timeout
            result = await fetch("slow-service")
            return result
    except TimeoutError:
        return "TIMED_OUT"


# - Never block the loop -


def cpu_bound_work(n: int) -> int:
    """Synchronous, CPU-intensive work."""
    return sum(i * i for i in range(n))


async def offload_to_thread_demo() -> int:
    """Move CPU work off the event loop thread."""
    return await asyncio.to_thread(cpu_bound_work, 10_000)


# - Locking across coroutines -


class AsyncCounter:
    """Thread-safe isn't async-safe - use asyncio.Lock for coroutines."""

    def __init__(self) -> None:
        self._value = 0
        self._lock = asyncio.Lock()

    async def increment(self) -> int:
        async with self._lock:
            self._value += 1
            return self._value


async def lock_demo() -> None:
    counter = AsyncCounter()
    results = await asyncio.gather(*[counter.increment() for _ in range(10)])
    print(f"  Final count: {results[-1]} (expected: 10)")


# - Main -


async def main() -> None:
    print("=== gather ===")
    results = await gather_demo()
    print(f"  Retrieved {len(results)} results")

    print("\n=== tasks ===")
    await task_demo()

    print("\n=== timeout ===")
    result = await timeout_demo()
    print(f"  Result: {result}")

    print("\n=== to_thread ===")
    value = await offload_to_thread_demo()
    print(f"  CPU result: {value}")

    print("\n=== lock ===")
    await lock_demo()


if __name__ == "__main__":
    asyncio.run(main())