asyncio 里最常见的三个错误:阻塞调用、超时与取消
asyncio 的性能前提是事件循环从不被阻塞。下面三类问题几乎每次写异步代码都会遇到。
一、同步库阻塞事件循环
import asyncio, requests
async def bad(url):
return requests.get(url).text # 阻塞整个循环
async def good(url):
async with aiohttp.ClientSession() as s:
async with s.get(url) as r:
return await r.text()判断方法:协程里出现 requests、time.sleep、同步数据库驱动、open() 大文件读写,基本就是阻塞点。确实必须用同步库时,套 asyncio.to_thread()。
二、超时要取消到底
# 只等结果,不等取消完成
try:
data = await asyncio.wait_for(fetch(), timeout=3)
except asyncio.TimeoutError:
...
# 更稳:3.11+ 的 timeout 会确保取消传播
async with asyncio.timeout(3):
data = await fetch()wait_for 会取消内部任务,但若协程里有 finally 阻塞或吞掉 CancelledError,超时会“假成功”。协程里捕获异常时不要吞 BaseException,也不要在 finally 里做长时间同步操作。
三、异常聚合与结构化并发
async with asyncio.TaskGroup() as tg: # 3.11+
t1 = tg.create_task(fetch(1))
t2 = tg.create_task(fetch(2))
# 任一任务失败 -> 其余被取消,异常以 ExceptionGroup 抛出相比 gather(return_exceptions=True),TaskGroup 的语义是“要么全成,要么全撤”,更适合有依赖关系的批量任务;需要部分成功时再显式收集异常。
小结
- 协程里禁止同步阻塞调用,必要时 to_thread
- 超时要用可传播取消的写法,别吞 CancelledError
- 批量任务优先 TaskGroup,明确失败语义