协程并发实现原理解析:await与asyncio.gather的适用场景
两种异步测试场景的运行机制解析及
await vs asyncio.gather的使用时机 先看你代码里的核心问题:asleep函数虽然用async定义,但内部调用的是time.sleep(n)——这是同步阻塞的IO操作,它会直接卡住整个Python线程,不让事件循环有机会切换到其他协程。这是理解两个场景差异的关键前提。
场景1:await asleep(2)(无并发)
当事件循环启动后,会按顺序调度任务:
- 先执行
doAsync1,打印doAsync1 start...,然后走到await asleep(2)。 - 进入
asleep后,time.sleep(2)直接阻塞线程,事件循环完全被卡住,根本没机会切换到doAsync2或doAsync3。 - 2秒后阻塞结束,
doAsync1打印doAsync1 end...并完成。 - 事件循环才会调度下一个任务
doAsync2,重复上述步骤,直到三个任务全部完成。
最终呈现串行执行的效果,总耗时6秒——这不是await本身的问题,是因为asleep里的阻塞操作让协程根本没机会挂起释放事件循环。
场景2:await asyncio.gather(asleep(2))(伪并发)
这里的关键是asyncio.gather的行为:它会把传入的可等待对象注册到事件循环,然后当前协程主动挂起,让事件循环去调度其他就绪的任务:
- 事件循环先调度
doAsync1,打印doAsync1 start...,执行到await asyncio.gather(asleep(2))时,doAsync1挂起,事件循环切换到doAsync2。 doAsync2打印doAsync2 start...,同样执行到await gather(...)时挂起,事件循环切换到doAsync3。doAsync3打印doAsync3 start...,执行到await gather(...)时挂起。- 此时所有任务都处于等待状态,事件循环会唤醒第一个等待的任务(比如
doAsync1),执行asleep(2)里的time.sleep(2),再次阻塞线程2秒。 - 阻塞结束后
doAsync1完成,事件循环唤醒doAsync2,执行它的time.sleep(2),再阻塞2秒;接着是doAsync3的2秒阻塞。 - 最后三个任务依次完成,打印各自的
end...。
你看到的“三个start同时输出”,是因为asyncio.gather让每个doAsync任务在遇到await时主动挂起,给了其他任务执行的机会。但因为time.sleep的阻塞特性,实际的asleep操作还是串行的,总耗时还是6秒——如果把time.sleep(n)换成异步的await asyncio.sleep(n),那三个asleep(2)会真的并发执行,总耗时就变成2秒了。
什么时候用await some_work(),什么时候用await asyncio.gather(...)
用await some_work()的场景
- 你需要按顺序执行单个协程,当前任务的后续代码依赖这个协程的执行结果或状态。比如:
async def fetch_user_data(user_id): data = await get_user_from_db(user_id) # 必须拿到数据才能继续处理 await update_user_profile(data) - 处理单个异步操作,不需要并发执行其他任务时。
用await asyncio.gather(...)的场景
- 你有多个独立的异步任务,它们之间没有依赖关系,可以同时执行来提升效率。比如同时请求多个第三方API:
async def fetch_multiple_data(): tasks = [fetch_api(url1), fetch_api(url2), fetch_api(url3)] results = await asyncio.gather(*tasks) # 三个请求并发执行,总耗时等于最慢的那个请求 process_results(results) - 需要等待一组异步任务全部完成后,再统一处理结果时。
内容的提问来源于stack exchange,提问作者leo
相关产品推荐
相关产品推荐

