asyncio.run() 报 RecursionError 是因协程内同步递归(如未 await 就自调用),导致 Python 栈溢出;应改用 while 循环+await 显式让权,或用 create_task 打破调用链,避免隐式递归累积栈帧。

asyncio.run() 启动异步主函数时爆 RecursionError: maximum recursion depth exceeded
这不是 asyncio 本身的递归问题,而是你在 async def 函数里写了同步递归调用(比如在 await 前就直接调用了自己),导致 Python 解释器栈不断增长。asyncio 不会“展开”你的递归调用,它只是调度协程对象——如果协程内部是同步递归,照样会撞上默认的 sys.getrecursionlimit()(通常 1000)。
常见错误写法:
async def fetch_data(url, retries=3):
try:
return await httpx.AsyncClient().get(url)
except Exception:
if retries > 0:
return await fetch_data(url, retries - 1) # ✅ 看似 async,实则同步递归链
raise
上面代码每次失败都会新建一个协程帧并压栈,但控制流没交还事件循环,retries 深度大时必然溢出。
- 别用
sys.setrecursionlimit()治标:它扩大栈空间但不解决根本问题,且可能引发段错误或内存耗尽 - 真正该做的是把递归逻辑拆成事件循环可调度的步骤:每次重试都
await一个明确的暂停点(如asyncio.sleep()或下一次fetch_data调用) - 更稳妥的做法是直接改写为 while 循环 + 状态变量
用 while 替代递归:保持 await 可中断性
异步循环的关键是让每次迭代都成为事件循环的一个调度单元,避免长调用链阻塞。
上面重试逻辑的正确写法:
import asyncio import httpx <p>async def fetch_with_retry(url, max_retries=3, delay=1): for attempt in range(max_retries + 1): try: async with httpx.AsyncClient() as client: return await client.get(url) except Exception as e: if attempt == max_retries: raise e await asyncio.sleep(delay * (2 ** attempt)) # 指数退避 return None</p>
-
for循环本身不增加 Python 栈深度,每次await都会挂起当前协程、交出控制权 - 指数退避中的
await asyncio.sleep()是必须的:它不仅是延迟,更是显式让出 CPU,防止密集轮询打满事件循环 - 注意
httpx.AsyncClient()实例应尽量复用或用async with包裹,避免在循环内反复创建连接对象
深层嵌套 await 链导致隐式递归(如 A → B → C → A)
当多个协程互相 await 形成闭环(哪怕间接),且没有中间 await 点释放控制权,也可能触发递归限制。典型场景是状态机驱动的异步解析器、自引用的异步生成器、或错误地将回调注册为 await 而非 create_task()。
- 检查调用图:用
asyncio.current_task().get_coro()+inspect.getframeinfo()打印调用栈,确认是否出现重复协程名反复出现 - 闭环中至少一处必须用
asyncio.create_task()而非await,把后续执行推入事件循环队列,打破同步调用链 - 例如:A
awaits B,Bawaits C,C 又想触发 A 的下一轮处理 → 此时 C 应该asyncio.create_task(A()),而不是await A()
async for / async with 中误用递归
在异步迭代器或上下文管理器内部写递归,容易忽略 __aiter__/__anext__ 方法本身是同步方法,它们返回协程对象,但递归调用仍走 Python 栈。
错误示例(异步生成器中递归):
async def walk_tree(node):
yield node
for child in node.children:
async for x in walk_tree(child): # ❌ 这里会累积栈帧
yield x
正确做法是扁平化为迭代+栈结构:
async def walk_tree_iter(node):
stack = [node]
while stack:
curr = stack.pop()
yield curr
stack.extend(reversed(curr.children))
- 异步生成器的
async for本质是反复调用__anext__,每次调用都是独立协程帧;但如果__anext__内部递归调用自身,栈还是会涨 - 用显式
stack或queue替代函数调用栈,把“递归深度”转为堆内存占用,更可控 - 注意
reversed()是为了保持原递归顺序(DFS 先序),按需调整
递归深度溢出在异步代码里往往不是“深度太大”,而是“根本没让事件循环插手”。最危险的其实是那些看起来很 async、实际却锁死解释器栈的写法——它们不会报 asyncio 相关错误,只安静地崩给你看。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











