Python asyncio长时计算场景下任务调度异常问题咨询
Asyncio协程调度异常问题解析
问题描述
我正在使用Python的asyncio库,遇到了无法理解的行为。我的代码包含两个异步函数:f1执行长时间计算,f2是带有短sleep的简单函数,代码如下:
import asyncio import time async def f1(): print("f1 start") while True: print("f1 1") await asyncio.sleep(0) start = time.time() [3**100 for _ in range(8000000)] print("f1 2 elapsed time: ", time.time() - start) async def f2(): print("f2 start") await asyncio.sleep(0.01) print("f2 end") async def main(): tasks = [ f1(), f2(), ] await asyncio.gather(*tasks) if __name__ == "__main__": asyncio.run(main())
预期f2在0.01秒sleep后快速执行完成,即使f1在运行计算。但实际输出为:
f1 start f1 1 f2 start f1 2 elapsed time: 4.705456018447876 f1 1 f1 2 elapsed time: 4.23239803314209 f1 1 f1 2 elapsed time: 4.644515037536621 f1 1 f2 end f1 2 elapsed time: 4.337735891342163 f1 1
看起来f2要等f1完成数次迭代后才结束。原本认为f1中的await asyncio.sleep(0)会触发协程切换让f2运行,但未如预期。
原因分析
asyncio基于单线程事件循环工作,只有当协程遇到await、async for等可等待对象时,才会暂停当前协程,将控制权交还给事件循环,由事件循环调度其他就绪协程。
你代码中的[3**100 for _ in range(8000000)]是纯CPU密集型的同步代码,这段代码执行时会完全占用线程资源,事件循环根本没有机会去检查其他协程的状态(比如f2的休眠是否到期)。
具体执行流程:
- 事件循环启动后,先调度
f1执行,打印"f1 start"和"f1 1",遇到await asyncio.sleep(0),控制权交还给事件循环。 - 事件循环调度
f2执行,打印"f2 start",遇到await asyncio.sleep(0.01),f2进入休眠状态,控制权回到事件循环。 - 事件循环再次调度
f1,此时f1开始执行那个耗时的列表推导——这步是同步计算,会连续占用CPU4秒以上,期间事件循环被完全阻塞,无法去检查f2的休眠是否已经结束。 - 直到
f1完成列表推导,打印耗时,再次循环到await asyncio.sleep(0),事件循环才终于有机会检查f2的状态,此时f2的0.01秒休眠早已到期,才会执行"f2 end"。
解决办法
要避免CPU密集型任务阻塞事件循环,常用两种方案:
方案1:使用线程/进程池执行CPU密集型任务
将同步计算任务丢到线程池或进程池,让事件循环可以继续调度其他协程。Python 3.9+可以用asyncio.to_thread(),也可以用loop.run_in_executor()兼容更低版本:
async def f1(): print("f1 start") loop = asyncio.get_running_loop() while True: print("f1 1") await asyncio.sleep(0) start = time.time() # 将同步计算任务提交到线程池执行 await loop.run_in_executor(None, lambda: [3**100 for _ in range(8000000)]) print("f1 2 elapsed time: ", time.time() - start)
方案2:拆分CPU密集型任务(不推荐)
手动将大计算任务拆分成多个小任务,在每个小任务之间插入await asyncio.sleep(0)主动让出CPU。这种方式需要修改计算逻辑,实用性较低,仅适用于无法使用线程/进程池的场景。
内容的提问来源于stack exchange,提问作者mtkvf
相关产品推荐
相关产品推荐

