为何无await asyncio.sleep(0)时aiohttp请求会阻塞任务创建?
await asyncio.sleep(0)时,aiohttp请求会阻塞任务创建流程? 现象说明
当注释掉代码中的await asyncio.sleep(0)(Label 1行)时,程序会先连续打印大量的init,之后才混合出现hello和bye;保留该行时,init、hello、bye会按异步任务的预期混合输出。另外,直接通过生成器表达式传递协程给asyncio.gather的写法,也会出现同样的异常现象。
示例代码
带sleep的版本
import asyncio from aiohttp import ClientSession async def download(session: ClientSession, link: str): print("init") async with session.get(link) as resp: print("hello") data = await resp.content.read() print("bye") return async def amain(): link = "https://www.google.com/" async with ClientSession() as session: tasks = [] for _ in range(100_000): tasks.append(asyncio.create_task(download(session, link))) await asyncio.sleep(0) # <------------ Label 1 await asyncio.gather(*tasks) if __name__ == '__main__': asyncio.run(amain())
异常写法(gather直接传生成器)
async def amain(): link = "https://www.google.com/" async with ClientSession() as session: await asyncio.gather(*(download(session, link) for _ in range(100_000)))
原因解析
核心在于asyncio是单线程协作式事件循环,所有任务的调度完全依赖当前代码主动让出CPU(即遇到await关键字):
无sleep的情况:
循环会一口气创建10万个任务并加入队列,但当前执行流还在amain的循环里,事件循环没有机会切换到其他任务。等所有任务都创建完成后,事件循环才开始逐个调度任务。
每个download任务执行时,print("init")是同步代码会立刻执行,而session.get内部存在不少同步初始化逻辑(比如连接池检查、请求头组装等),这些代码会在真正触发网络IO的await前执行完毕。只有当某个任务走到网络IO等待的await时,才会让出CPU给其他任务。这就导致大量任务先执行到init和session.get的同步部分,所以先批量打印init,之后才开始混合出现hello和bye。有sleep的情况:
每创建一个任务就执行await asyncio.sleep(0),这个操作会主动让出CPU,事件循环会立刻调度刚创建的download任务。该任务执行到网络IO的await时又会让出CPU,回到amain的循环创建下一个任务,以此往复,所以init、hello、bye会交替输出。gather直接传生成器的情况:
生成器表达式会先一次性生成10万个协程对象,之后gather才会把它们包装成任务并提交给事件循环。这和无sleep的场景本质一样——先完成所有任务/协程的创建,再统一调度,因此出现同样的批量输出init的现象。
内容的提问来源于stack exchange,提问作者LtGenFlower

