← 中文资源

简体中文版本

Python asyncio 超时 — TimeoutError、事件循环阻塞与慢任务

测量协程耗时,定位阻塞事件循环的同步调用,检查下游延迟与并发上限,用显式超时预算和信号量修复 asyncio 超时。

阅读英文原文

现象

asyncio.wait_for 或 asyncio.timeout 抛出 TimeoutError;单个请求很快,并发时大量超时;事件循环像卡死一样,其他协程也停止推进;取消后出现 Task exception was never retrieved 警告。

诊断

先测量单次调用耗时:

python -X dev -c "import asyncio, time; from app.client import fetch; t = time.perf_counter(); asyncio.run(fetch()); print('elapsed', time.perf_counter() - t)"
PYTHONASYNCIODEBUG=1 python -m app.main 2>&1 | grep -Ei 'slow callback|took [0-9.]+ seconds|never awaited' | head -20
py-spy top --pid $(pgrep -f "python -m app.main")

若单次调用就超过阈值,问题在超时值或下游延迟;若日志显示回调阻塞事件循环,或在 py-spy 中看到大量时间停在 time.sleep、同步 socket 读取,则说明协程里混入了阻塞调用,所有任务的截止时间会一起到期。并发场景还要检查连接池大小与信号量,工作可能一直排队,请求根本没发出去。

修复

为每个网络调用设置连接、读取与总超时,用 asyncio.timeout 包住整个操作,阻塞库改走 asyncio.to_thread 或换成异步客户端;用信号量限制并发并与连接池匹配;只对幂等操作做带退避的重试,且不要用裸 except 吞掉 CancelledError:

async with sem:
    async with asyncio.timeout(2.5):
        response = await client.get(url)
        response.raise_for_status()
        return response.json()

验证

重新压测,确认正常流量下 p95 低于超时阈值且不再出现 TimeoutError;人为注入慢依赖,验证超时能按时触发、任务被干净取消、关闭时没有未取回异常。最后确认非幂等接口在超时后没有被重复写入。