
CPU 密集型任务会完全阻塞 asyncio 事件循环,导致其他协程无法调度;但 asyncio.sleep() 等 IO 协程的“唤醒时间”是绝对时点而非相对延迟,因此一旦 CPU 任务结束,已超时的 sleep 会立即完成,而非额外等待。
cpu 密集型任务会完全阻塞 asyncio 事件循环,导致其他协程无法调度;但 `asyncio.sleep()` 等 io 协程的“唤醒时间”是绝对时点而非相对延迟,因此一旦 cpu 任务结束,已超时的 sleep 会立即完成,而非额外等待。
在 asyncio 中,事件循环是单线程、协作式调度的——它依赖协程主动让出控制权(如通过 await)才能切换任务。关键在于:CPU 密集型任务本身不是协程,也不包含任何 await 表达式,因此一旦开始执行,就会独占当前线程,直到函数返回,期间事件循环完全停滞。
以你的原始实验为例:
async def main():
await io_bound(1) # ✅ 执行并挂起 1 秒,事件循环可调度其他任务
asyncio.create_task(cpu_bound()) # ? 启动 CPU 任务(作为后台 task)
await io_bound(2) # ❌ 此处会等待 io_bound(2) 完成 —— 但它的 await asyncio.sleep(1) 已被“预设”了唤醒时间点这里存在一个常见误解:await asyncio.sleep(1) 并非“休眠 1 秒后唤醒”,而是 “注册一个在‘当前时间 + 1 秒’触发的回调”。事件循环内部维护一个最小堆(priority queue)记录所有待唤醒的定时器。当你调用 await asyncio.sleep(1) 时,事件循环将该协程挂起,并把它的恢复时间(例如 t0 + 1.0)插入定时器队列。
然而,如果此时事件循环被 CPU 任务阻塞(如 for i in range(5*10**7): ...),它就无法在 t0 + 1.0 时刻检查定时器。等到 cpu_bound() 终于执行完毕(比如耗时 5 秒后),事件循环重新获得控制权,立刻扫描定时器队列——发现 io_bound(2) 的唤醒时间 t0 + 1.0 已经过去 4 秒,于是立即恢复该协程,不额外等待。这就是你观察到 finish io_bound 2 紧跟在 finish cpu_bound 之后的原因:它不是“睡了 1 秒”,而是“终于等到被唤醒”。
✅ 验证这一点的最简方式:在 CPU 任务前显式插入一次 await asyncio.sleep(0) 或 await asyncio.sleep(0.0001),强制让出控制权,使 io_bound(2) 的 sleep 定时器能被正确注册和监控:
async def main():
await io_bound(1)
task = asyncio.create_task(cpu_bound())
await asyncio.sleep(0.0001) # ⚠️ 关键:让事件循环有机会注册 io_bound(2) 的 timer
await io_bound(2)
await task # 确保 cpu_bound 完成(原代码中未 await,存在竞态)⚠️ 注意事项:
-
asyncio.create_task()只是提交任务到事件循环的任务队列,并不立即执行;实际执行仍需事件循环空闲时调度。 - 原始代码中
await io_bound(2)是同步等待,而cpu_bound()是异步启动但未await,这导致io_bound(2)实际上是在cpu_bound()启动后才开始执行其await asyncio.sleep(1)—— 因此它的定时器注册时间点晚于预期。 - 真实 IO 操作(如
aiohttp请求)同样受此机制影响,但网络延迟本身具有不确定性;而asyncio.sleep()是纯定时行为,更能暴露调度逻辑。
? 总结:
CPU 密集型任务绝对会阻塞事件循环,这不是“不阻塞”,而是阻塞后事件循环对已过期定时器的处理策略导致了看似“并发”的假象。要真正实现 CPU 与 IO 的并行,必须将 CPU 任务移出主线程——使用 loop.run_in_executor() 配合 concurrent.futures.ProcessPoolExecutor 或 ThreadPoolExecutor:
import asyncio
from concurrent.futures import ProcessPoolExecutor
def blocking_cpu_work():
for i in range(5 * 10**7):
i *= i + 1
i /= i + 1
return "done"
async def main():
await io_bound(1)
loop = asyncio.get_running_loop()
# 在进程池中执行 CPU 任务,不阻塞事件循环
await loop.run_in_executor(ProcessPoolExecutor(), blocking_cpu_work)
await io_bound(2)这才是 asyncio 推荐的 CPU/IO 混合场景正确解法。

















