68 lines
2.6 KiB
Markdown
68 lines
2.6 KiB
Markdown
---
|
|
title: "Python Async: Beyond the Basics"
|
|
date: 2026-05-12T11:00:00+08:00
|
|
draft: false
|
|
description: "Understanding the event loop, writing proper async context managers, and avoiding the most common pitfalls with asyncio.gather and cancel scopes."
|
|
tags:
|
|
- Python
|
|
- Async
|
|
- Programming
|
|
---
|
|
|
|
Async Python is no longer optional if you're doing any kind of I/O-bound work. Beyond the basics of `async`/`await`, understanding the event loop helps debug elusive issues.
|
|
|
|
## The Event Loop in Practice
|
|
|
|
The event loop runs one coroutine at a time, switching at `await` points. This is cooperative multitasking — a coroutine that never awaits blocks everything else. If you have CPU-bound work inside an async function, offload it to a thread pool with `asyncio.to_thread()`.
|
|
|
|
```python
|
|
import asyncio
|
|
|
|
# Bad: blocks the event loop
|
|
async def bad():
|
|
result = heavy_computation() # CPU-bound, no await
|
|
return result
|
|
|
|
# Good: offloads to a thread
|
|
async def good():
|
|
result = await asyncio.to_thread(heavy_computation)
|
|
return result
|
|
```
|
|
|
|
## Async Context Managers
|
|
|
|
Writing proper async context managers is essential for managing connections and locks:
|
|
|
|
```python
|
|
class AsyncConnection:
|
|
async def __aenter__(self):
|
|
self.conn = await create_connection()
|
|
return self.conn
|
|
|
|
async def __aexit__(self, *args):
|
|
await self.conn.close()
|
|
```
|
|
|
|
The `contextlib.asynccontextmanager` decorator makes this even cleaner for simple cases.
|
|
|
|
## Common Pitfalls with `asyncio.gather`
|
|
|
|
`gather` runs tasks concurrently, but one failing task doesn't cancel the others by default. If you need all-or-nothing semantics, use `asyncio.TaskGroup` (Python 3.11+):
|
|
|
|
```python
|
|
async with asyncio.TaskGroup() as tg:
|
|
t1 = tg.create_task(fetch(url1))
|
|
t2 = tg.create_task(fetch(url2))
|
|
# Both succeeded, or both were cancelled
|
|
```
|
|
|
|
## Cancel Scopes
|
|
|
|
Cancellation in asyncio is cooperative: `asyncio.CancelledError` is raised at the next `await`. If your coroutine catches and suppresses it, the task becomes uncancellable — usually a bug. Use `asyncio.shield()` sparingly to protect critical cleanup code.
|
|
|
|
## Structured Concurrency
|
|
|
|
The trend across languages (Trio in Python, Kotlin coroutines, Swift concurrency) is toward structured concurrency: every task has a clear parent scope that determines its lifetime. `TaskGroup` is Python's step in this direction. Embrace it over bare `create_task()` calls.
|
|
|
|
Async Python has matured significantly. Most of the old footguns have been addressed, but the fundamentals — understand the event loop, respect cancellation, and structure your concurrency — remain essential.
|