You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何Python中await递归会触发递归深度超限异常?

问题:为何异步递归代码会触发递归深度超限异常?

我知晓生产环境中绝不应编写如下这类不良代码,以下仅作示例说明。

为什么这段代码会触发递归深度超限异常?

import asyncio
import logging
import sys

sys.setrecursionlimit(19)  # to make it occurr quickly

async def main(num: int = 0):
    print(num)
    await asyncio.sleep(0.0001)
    await main(num + 1)

if __name__ == "__main__":
    logging.basicConfig(level=logging.DEBUG)
    asyncio.run(main())

运行输出:

# $ python tmp.py 
DEBUG:asyncio:Using selector: EpollSelector
0
1
2
3
4
5
6
7
Traceback (most recent call last):
  File "/data/projects/data1/xxx/yyy/tmp.py", line 16, in <module>
    asyncio.run(main())
  File "/data/users/xxx/miniconda3/lib/python3.12/asyncio/runners.py", line 194, in run
    return runner.run(main)
           ^^^^^^^^^^^^^^^^
  File "/data/users/xxx/miniconda3/lib/python3.12/asyncio/runners.py", line 118, in run
    return self._loop.run_until_complete(task)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/data/users/xxx/miniconda3/lib/python3.12/asyncio/base_events.py", line 687, in run_until_complete
    return future.result()
           ^^^^^^^^^^^^^^^
  File "/data/projects/data1/xxx/yyy/tmp.py", line 11, in main
    await main(num + 1)
  File "/data/projects/data1/xxx/yyy/tmp.py", line 11, in main
    await main(num + 1)
  File "/data/projects/data1/xxx/yyy/tmp.py", line 11, in main
    await main(num + 1)
  [Previous line repeated 4 more times]
  File "/data/projects/data1/xxx/yyy/tmp.py", line 10, in main
    await asyncio.sleep(0.0001)
  File "/data/users/xxx/miniconda3/lib/python3.12/asyncio/tasks.py", line 661, in sleep
    h = loop.call_later(delay,
        ^^^^^^^^^^^^^^^^^^^^^^
  File "/data/users/xxx/miniconda3/lib/python3.12/asyncio/base_events.py", line 761, in call_later
    timer = self.call_at(self.time() + delay, callback, *args,
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/data/users/xxx/miniconda3/lib/python3.12/asyncio/base_events.py", line 778, in call_at
    timer = events.TimerHandle(when, callback, args, self, context)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/data/users/xxx/miniconda3/lib/python3.12/asyncio/events.py", line 112, in __init__
    super().__init__(callback, args, loop, context)
  File "/data/users/xxx/miniconda3/lib/python3.12/asyncio/events.py", line 45, in __init__
    if self._loop.get_debug():
       ^^^^^^^^^^^^^^^^^^^^^^
RecursionError: maximum recursion depth exceeded

这是个无终止条件的不良递归函数,同步调用时触发递归超限很正常——每个函数调用的栈帧直接嵌套在调用者栈帧里,层级很快就失控。但用了异步机制后,我原本以为每个函数的栈帧应该是独立的,由事件循环通过回调管理依赖,就算有问题也应该是内存耗尽而非递归超限。可实际情况是,await在这里就像同步调用一样,让外层main的栈帧一直维持着和被调用者的依赖,栈信息也显示了嵌套调用:

File "/data/projects/data1/xxx/yyy/tmp.py", line 11, in main
    await main(num + 1)
  File "/data/projects/data1/xxx/yyy/tmp.py", line 11, in main
    await main(num + 1)
  File "/data/projects/data1/xxx/yyy/tmp.py", line 11, in main
    await main(num + 1)
  [Previous line repeated 4 more times]

另外我知道把await main(num+1)改成asyncio.create_task(main(num+1))就能消除协程间的依赖,不会触发递归超限,这符合我的预期。


解答

核心原因:await会保留调用栈帧直到被等待的协程完成

异步协程的await关键字并不是立即释放当前栈帧,而是暂停当前协程的执行,保留栈帧,等待被await的对象完成后再恢复当前协程的执行。

在你的代码里,main(num)执行到await main(num+1)时:

  1. 先创建main(num+1)协程对象,然后await它
  2. 此时当前main(num)的栈帧并不会被销毁,而是被挂起,等待main(num+1)执行完毕
  3. 但main(num+1)执行到最后又会await main(num+2),同样挂起自己的栈帧等待下一个协程完成
  4. 以此类推,每一层main的栈帧都被保留,直到递归深度达到Python的递归限制,最终触发RecursionError

简单说,这种写法本质上是同步式的递归调用,只是套了异步协程的壳——每一层调用都要等下一层完成才能继续,栈帧自然会像同步递归一样累积。

为什么asyncio.create_task不会触发递归超限?

asyncio.create_task的作用是把协程提交给事件循环调度,立即返回一个Task对象,当前协程不需要等待这个新协程完成就能继续执行(甚至直接结束)。

当你把await main(num+1)改成asyncio.create_task(main(num+1))后:

  1. main(num)创建并提交main(num+1)给事件循环,之后自身协程执行完毕,栈帧被销毁
  2. 事件循环会在合适的时机调度main(num+1)执行,它再提交main(num+2)后自身也会结束,栈帧销毁
  3. 每一层协程的栈帧都不会被保留,自然不会出现递归栈累积的问题,只会不断创建新的Task对象,直到内存耗尽(但不会触发递归超限)

内容的提问来源于stack exchange,提问作者Isuxiz Slidder

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.12 20:28:12