python 3.11 的 asyncio 调试能力未新增 api,但底层帧优化提升了协程状态的稳定性与可读性;await 行号不可见是因内联生成 call_intrinsic_1 字节码,属正常性能取舍。

Python 3.11 的 asyncio 调试能力本身没加新 API,但底层帧优化让协程状态在调试器里更稳定、更易读;关键不是“怎么开调试器”,而是“哪些状态能信、哪些会骗你”。
为什么 pdb / VS Code 断点进协程后看不到 await 行号?
这是 Python 3.11 之前就存在的问题,3.11 并未修复,但行为更可预测:因为 await 指令被内联(生成 CALL_INTRINSIC_1 字节码),调试器可能停在上一行或下一行,而非 await 本身。这不是 bug,是解释器为提速做的取舍。
- 用
import dis; dis.dis(your_coro)看字节码,若含CALL_INTRINSIC_1,说明已启用内联路径,此时断点位置偏差属正常 - 想准确定位 await 点?在
await前加一句_ = 1或pass,强制插入可断点的指令 - VS Code 中确保
python.defaultInterpreterPath指向真实 3.11 解释器,否则调试器仍按旧帧模型解析
如何用 asyncio.debug 查看运行中协程栈?
asyncio.debug 不是模块名,而是 asyncio 的一个内部调试开关,需通过环境变量或代码显式开启。它输出的是事件循环当前所有待调度协程的帧快照,不是实时堆栈。
- 启动前设环境变量:
PYTHONASYNCIODEBUG=1,再跑asyncio.run(main()),会打印每个 pending task 的coro.cr_frame.f_lineno - 代码中动态开启:
asyncio.get_event_loop().set_debug(True),但仅对之后创建的 task 生效 - 注意:开启后性能下降明显,仅用于排查 hang 住的任务,别留在线上
- 输出里看到
coro_state='CORO_RUNNING'是正常的;若长期卡在CORO_SUSPENDED且无后续调度,大概率是某个await对象永远不 resolve(比如漏了asyncio.sleep(0)触发让出)
用 sys._current_frames() 抓取协程原始帧对象时要注意什么?
这个函数返回所有线程当前帧,包括 asyncio 主线程里的协程帧,但它不区分协程和普通函数帧——你得自己过滤。
- 协程帧的
f_code.co_flags & 0x80为真(即含CO_COROUTINE标志),可用此判断 - 直接读
f_locals可能看到未完成的await表达式变量,但不要依赖f_lasti定位行号,3.11 中它可能指向内联后的跳转目标而非源码行 - 若看到多个帧的
f_code.co_name == 'run_until_complete',说明有 task 卡在事件循环入口,大概率是子任务没被 await 或被意外取消 - 别在生产环境高频调用它——每次调用都触发全栈扫描,CPU 开销不小
最常被忽略的一点:3.11 的帧优化让协程对象构造更快,但也让“刚创建还没 await”的协程帧更难捕获——它们存在时间极短,sys._current_frames() 很可能错过。真要查初始化问题,得在 async def 函数体第一行就下断点,而不是等它跑起来再抓。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











