Saltar a contenido

Week 13 - asyncio Foundations: Event Loop, Tasks, Coroutines

13.1 Conceptual Core

  • An async function is a function that returns a coroutine object. Awaiting yields control to the event loop. The event loop drives many coroutines, switching at every await.
  • The cardinal sin: blocking the event loop. A single time.sleep(1), sync DB call, or CPU-heavy loop in a coroutine stalls every other task. This is the most common production asyncio bug.
  • Task vs. Coroutine. A coroutine is a description; a Task is a coroutine scheduled on the loop. await coro runs it inline; asyncio.create_task(coro) runs it concurrently and returns a handle.

13.2 Mechanical Detail

  • asyncio.run, asyncio.create_task, asyncio.gather, asyncio.wait, asyncio.as_completed, asyncio.wait_for, asyncio.TaskGroup (3.11+, the way to write structured concurrency since 3.11).
  • async with, async for, async generators, __aenter__/__aexit__, __aiter__/__anext__.
  • asyncio.Queue, asyncio.Lock, asyncio.Event, asyncio.Semaphore, asyncio.Condition. None are thread-safe; for thread-safe inter-loop comm, use asyncio.run_coroutine_threadsafe or janus.
  • Cancellation is cooperative and exception-based: task.cancel() injects CancelledError at the next await. Code that catches Exception swallowing CancelledError is the asyncio anti-pattern; in 3.11+, CancelledError is no longer a subclass of Exception - but old code remains.
  • Timeouts: async with asyncio.timeout(5): (3.11+) is the idiomatic form. asyncio.wait_for is older and has subtle cancellation pitfalls.
  • loop.run_in_executor(None, blocking_fn, args): the escape hatch for blocking calls. Use for legacy DB drivers, file I/O if not using aiofiles, and CPU work.

13.3 Lab - "The Crawler That Doesn't Lie"

  1. Build an async HTTP crawler with httpx.AsyncClient and a TaskGroup. Limit concurrency with a Semaphore(N).
  2. Add a 5-second per-request timeout using asyncio.timeout. Verify cancellation propagates cleanly to the httpx request.
  3. Inject a deliberately blocking time.sleep(2) somewhere. Detect it with asyncio.get_event_loop().slow_callback_duration = 0.1 and the resulting log warnings.
  4. Replace the blocker with asyncio.sleep. Confirm via py-spy dump that the loop never stalls.

13.4 Idiomatic & Linter Drill

  • Enable ruff ASYNC rule set in full. Catch every blocking call inside async def.

13.5 Production Hardening Slice

  • Add aiomonitor or aiodebug to your dev environment. Add a request-id ContextVar and structured logging that propagates across await boundaries.

Comments