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

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的休眠是否到期)。

具体执行流程:

  1. 事件循环启动后,先调度f1执行,打印"f1 start"和"f1 1",遇到await asyncio.sleep(0),控制权交还给事件循环。
  2. 事件循环调度f2执行,打印"f2 start",遇到await asyncio.sleep(0.01),f2进入休眠状态,控制权回到事件循环。
  3. 事件循环再次调度f1,此时f1开始执行那个耗时的列表推导——这步是同步计算,会连续占用CPU4秒以上,期间事件循环被完全阻塞,无法去检查f2的休眠是否已经结束。
  4. 直到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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 16:50:37