为何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)时:
- 先创建
main(num+1)协程对象,然后await它 - 此时当前
main(num)的栈帧并不会被销毁,而是被挂起,等待main(num+1)执行完毕 - 但
main(num+1)执行到最后又会await main(num+2),同样挂起自己的栈帧等待下一个协程完成 - 以此类推,每一层
main的栈帧都被保留,直到递归深度达到Python的递归限制,最终触发RecursionError
简单说,这种写法本质上是同步式的递归调用,只是套了异步协程的壳——每一层调用都要等下一层完成才能继续,栈帧自然会像同步递归一样累积。
为什么asyncio.create_task不会触发递归超限?
asyncio.create_task的作用是把协程提交给事件循环调度,立即返回一个Task对象,当前协程不需要等待这个新协程完成就能继续执行(甚至直接结束)。
当你把await main(num+1)改成asyncio.create_task(main(num+1))后:
main(num)创建并提交main(num+1)给事件循环,之后自身协程执行完毕,栈帧被销毁- 事件循环会在合适的时机调度
main(num+1)执行,它再提交main(num+2)后自身也会结束,栈帧销毁 - 每一层协程的栈帧都不会被保留,自然不会出现递归栈累积的问题,只会不断创建新的
Task对象,直到内存耗尽(但不会触发递归超限)
内容的提问来源于stack exchange,提问作者Isuxiz Slidder
相关产品推荐
相关产品推荐

