Python asyncio底层实现是否涉及线程机制?
问题描述
更新: 本问题的提出前提暴露了我此前对相关概念理解不足,下方回答颇具启发,但本问题的核心假设本身完全错误。
我正在自学Python异步执行模型,如下示例程序我原本误以为可以异步下载5个不同网页:
#!/usr/bin/env python3 import requests import asyncio async def download(url): response = requests.get(url) print(f"Have downloaded {url}") async def async_main(): for url in ["https://www.aftenposten.no", "https://www.vg.no", "https://lwn.net", "https://www.dagbladet.no", "https://www.nrk.no"]: await download(url) loop = asyncio.get_event_loop() loop.run_until_complete(async_main())
该程序大致达到了预期加速效果,运行符合预期,一切正常 但我始终无法理解await download(url)行的执行逻辑。我此前的粗浅认知是执行流程如下:
download(url)函数会“在后台”被调用;- 事件循环暂停当前协程实例,启动执行下一个协程。
但要实现上述逻辑,download(url)调用必须处于“某个执行上下文”中,因此我猜测asyncio内部实现是否采用了线程?即经过初始调度隔离后,asyncio会在独立执行上下文(即线程)中调用download(url)?这和文档中“asyncio并发模型不涉及多线程/多进程”的表述似乎存在矛盾,恳请得到清晰的解答。
更新: 评论与回答中均有用户质疑我提到的加速效果,我重新开展了更严谨的计时测试,发现此前结论错误——我大概率先入为主看到了自己预期的结果,更严谨的测试显示串行执行速度反而略快,为此前造成的困惑致歉。
解答
你的代码本质是完全串行执行的,没有任何异步并发效果,asyncio 默认也不会隐式创建线程来执行阻塞代码,你之前对await执行逻辑的认知存在偏差,具体拆解如下:
await的任务切换触发条件await不是遇到调用就自动切换任务,它只有在等待的对象主动让出执行权时,才会把控制权交回事件循环。如果等待的协程从头到尾没有执行任何挂起操作(比如没有调用asyncio.sleep、没有等待异步IO返回),那这个协程会直接同步跑完,整个过程事件循环根本没有机会调度其他任务。- 你的代码为什么是串行的
你定义的download虽然用async标记成了协程,但内部调用的requests.get是典型的同步阻塞方法:它执行时会直接占住当前线程等待网络响应,全程不会主动通知事件循环“我现在在等IO,你可以先跑别的任务”。
你在循环里写await download(url)的实际执行流是:- 进入当前轮次的
download协程 - 执行
requests.get,整个线程卡在网络请求上,直到响应返回 - 执行打印逻辑,协程运行结束
await拿到协程返回值,进入下一轮循环处理下一个URL
整个流程和你直接写普通同步for循环调用requests.get没有任何区别,甚至多了一层协程包装的调度开销,这也是你后续严谨测试发现串行速度反而更快的原因。
- 进入当前轮次的
- asyncio到底会不会用线程
asyncio 核心的单线程并发模型确实不默认依赖多线程:它的并发逻辑是单线程内的协作式调度,所有任务在遇到IO等待时主动挂起,把CPU让给其他就绪任务,等自己等待的IO就绪后再恢复执行,全程所有协程代码都跑在同一个事件循环线程里。
只有你显式调用asyncio.to_thread()、loop.run_in_executor()这类接口时,asyncio才会把指定的阻塞任务丢到独立的线程池/进程池里执行,避免卡住事件循环,这个行为完全是开发者主动触发的,不存在隐式的线程调度。 - 怎么实现真正的异步并发下载
你有两个可选方案:- 替换阻塞的HTTP库为异步实现,比如
aiohttp,所有IO操作都用异步原生接口,让事件循环可以在等待IO时自动切换任务 - 如果你坚持用
requests这类同步库,就显式把阻塞请求丢到线程池里执行,同时不要逐个await单个任务,而是把所有任务统一交给事件循环调度
以异步库实现为例,正确的并发写法如下:
#!/usr/bin/env python3 import asyncio import aiohttp async def download(session, url): async with session.get(url) as resp: await resp.read() # 等待响应内容时会主动挂起让出控制权 print(f"Have downloaded {url}") async def async_main(): urls = [ "https://www.aftenposten.no", "https://www.vg.no", "https://lwn.net", "https://www.dagbladet.no", "https://www.nrk.no" ] async with aiohttp.ClientSession() as session: # 创建所有任务,创建后就会开始被事件循环调度 tasks = [asyncio.create_task(download(session, url)) for url in urls] # 等待所有任务执行完成 await asyncio.gather(*tasks) if __name__ == "__main__": asyncio.run(async_main()) - 替换阻塞的HTTP库为异步实现,比如
最后要提醒一个常见误区:给函数加async关键字不会自动把函数变成非阻塞的。如果协程内部包含同步阻塞逻辑,又没有做额外的线程/进程隔离,运行时依然会卡住整个事件循环,和普通同步代码没有区别。
内容的提问来源于stack exchange,提问作者user422005
相关产品推荐
相关产品推荐

