为何CPU密集型任务未阻塞Python Asyncio事件循环?
为何CPU密集型任务未阻塞Python Asyncio事件循环?
嘿,这个问题其实是Asyncio事件循环调度逻辑的典型坑,我来给你一步步拆解~
首先看你第一个实验的执行流程:
- 你先
await io_bound(1),这个任务会完整执行完,耗时1秒到17:28:21。 - 接着调用
asyncio.create_task(cpu_bound())——注意,这只是把CPU任务加入事件循环的待执行队列,但事件循环此时还在main函数的执行流里,根本没机会切换到这个CPU任务。 - 紧接着你就
await io_bound(2):这个任务先打印启动日志,然后执行await asyncio.sleep(1)。这时候,事件循环终于有机会暂停当前的io_bound(2)任务,去调度队列里的其他任务——也就是刚加入的cpu_bound()。
但这里的关键来了:cpu_bound()是纯同步的CPU密集型代码,里面没有任何await语句。Asyncio的事件循环是单线程的,只有当任务遇到await(或者其他可暂停的点)时,才会让出CPU给其他任务。而你的cpu_bound()一旦开始执行,就会霸占整个线程,直到它跑完那5亿次循环(耗时约5秒),中间完全不会给事件循环任何调度其他任务的机会。
那为什么io_bound(2)会和cpu_bound()同时结束?其实是因为:
io_bound(2)的asyncio.sleep(1)在17:28:22就到期了,但此时事件循环被cpu_bound()死死占着,根本没法切换回io_bound(2)任务去执行后续的打印操作。- 直到17:28:26
cpu_bound()跑完,事件循环才终于解脱,立刻回到io_bound(2)任务,发现sleep已经到期,于是马上打印结束日志——所以看起来两者同时完成。
你的预期其实是对的:CPU密集型任务确实会阻塞事件循环,但第一个实验的执行顺序让你产生了误解。
后来你加入await asyncio.sleep(0.0001)的修改,正好命中了关键:
在创建cpu_bound()任务后,你加了一个极短的sleep,这会让事件循环立刻暂停main函数,优先执行队列里的cpu_bound()任务。这时候cpu_bound()霸占线程,直到它跑完,事件循环才回到main函数,继续执行await io_bound(2)——这时候io_bound(2)才真正开始执行,sleep1.5秒后完成,完全符合你预期的阻塞效果。
总结一下:
- Asyncio的单线程特性决定了,任何没有
await的同步代码(比如你的CPU密集型循环)都会阻塞整个事件循环。 - 任务调度的时机很重要:只有当当前任务遇到
await时,事件循环才会去处理队列里的其他任务。
备注:内容来源于stack exchange,提问作者Philipp
相关产品推荐
相关产品推荐

