Python asyncio 事件循环坑速查
> 由真实项目踩坑精选整理,按【坑/现象 → 根因 → 解法 → 适用场景】组织。
1. 在协程里调用同步阻塞库,冻结整个事件循环
【坑/现象】
- async 函数里用了 requests.get()、time.sleep()、同步文件读写或 CPU 重活,所有并发请求一起卡死;p99 延迟飙到阻塞调用时长,但平均延迟仍「正常」——因为只有恰好撞上阻塞的少数请求受害。
- 负载均衡健康检查因此失败,触发级联重启,放大事故。
【根因】
- asyncio 是协作式单线程:一个阻塞调用不 await 让出控制权,事件循环就停在那里,所有其他协程同时冻结。
【解法】
- 换异步库:requests → httpx/aiohttp,open() → aiofiles,psycopg2 → asyncpg。
- 换不了的同步活:await asyncio.to_thread(blocking_fn, *args)(3.9+,丢默认线程池);CPU 重活用 loop.run_in_executor(ProcessPoolExecutor(), fn)。
- 代码评审重点搜 time.sleep 和协程内的同步 I/O,逐一替换。
【适用场景】写 async 服务(FastAPI 等)、并发拉取多上游、本地 API 代理。
2. 漏 await、被 GC 的 fire-and-forget、gather 不带 return_exceptions
【坑/现象】
- do_work() 漏写 await,得到 coroutine 对象却啥也不干(仅 GC 时偶发 RuntimeWarning: coroutine was never awaited),无报错、无副作用、极难发现。
- asyncio.create_task(...) 不保存引用,任务执行中途被垃圾回收,表现成「偶发丢记录 / 步骤跳过」,依赖 GC 时机,几乎无法复现。
- asyncio.gather(a, b) 任一异常默认取消其余任务,留下孤儿任务持续占连接池/内存直到自己超时。
【根因】
- 协程对象未 await 永不执行;create_task 返回的 Task 无强引用会被 GC 提前取消;gather 默认语义在第一个异常处直接向上抛,兄弟任务失去 owner 变孤儿。
【解法】
- 协程一律 await,或交给 gather/create_task;绝不只调用不处理。
- fire-and-forget 保活:background_tasks = set(); task = asyncio.create_task(coro()); background_tasks.add(task); task.add_done_callback(background_tasks.discard)。
- 生产路径给 asyncio.gather(..., return_exceptions=True) 并逐个 isinstance(r, Exception) 处理。
- Python 3.11+ 优先用 asyncio.TaskGroup() 做结构化并发:一个失败自动取消兄弟并抛 ExceptionGroup。
【适用场景】后台任务、并发拉取多上游、定时批量作业、需要取消/超时控制的 async 流程。
> 作者:阿光 · 婚恋活动策划 × 摄影摄像 8 年实战。技能定制 / 业务合作:微信 13011137113