You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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)行的执行逻辑。我此前的粗浅认知是执行流程如下:

  1. download(url)函数会“在后台”被调用;
  2. 事件循环暂停当前协程实例,启动执行下一个协程。

但要实现上述逻辑,download(url)调用必须处于“某个执行上下文”中,因此我猜测asyncio内部实现是否采用了线程?即经过初始调度隔离后,asyncio会在独立执行上下文(即线程)中调用download(url)?这和文档中“asyncio并发模型不涉及多线程/多进程”的表述似乎存在矛盾,恳请得到清晰的解答。

更新: 评论与回答中均有用户质疑我提到的加速效果,我重新开展了更严谨的计时测试,发现此前结论错误——我大概率先入为主看到了自己预期的结果,更严谨的测试显示串行执行速度反而略快,为此前造成的困惑致歉。


解答

你的代码本质是完全串行执行的,没有任何异步并发效果,asyncio 默认也不会隐式创建线程来执行阻塞代码,你之前对await执行逻辑的认知存在偏差,具体拆解如下:

  1. await的任务切换触发条件
    await不是遇到调用就自动切换任务,它只有在等待的对象主动让出执行权时,才会把控制权交回事件循环。如果等待的协程从头到尾没有执行任何挂起操作(比如没有调用asyncio.sleep、没有等待异步IO返回),那这个协程会直接同步跑完,整个过程事件循环根本没有机会调度其他任务。
  2. 你的代码为什么是串行的
    你定义的download虽然用async标记成了协程,但内部调用的requests.get是典型的同步阻塞方法:它执行时会直接占住当前线程等待网络响应,全程不会主动通知事件循环“我现在在等IO,你可以先跑别的任务”。
    你在循环里写await download(url)的实际执行流是:
    • 进入当前轮次的download协程
    • 执行requests.get,整个线程卡在网络请求上,直到响应返回
    • 执行打印逻辑,协程运行结束
    • await拿到协程返回值,进入下一轮循环处理下一个URL
      整个流程和你直接写普通同步for循环调用requests.get没有任何区别,甚至多了一层协程包装的调度开销,这也是你后续严谨测试发现串行速度反而更快的原因。
  3. asyncio到底会不会用线程
    asyncio 核心的单线程并发模型确实不默认依赖多线程:它的并发逻辑是单线程内的协作式调度,所有任务在遇到IO等待时主动挂起,把CPU让给其他就绪任务,等自己等待的IO就绪后再恢复执行,全程所有协程代码都跑在同一个事件循环线程里。
    只有你显式调用asyncio.to_thread()、loop.run_in_executor()这类接口时,asyncio才会把指定的阻塞任务丢到独立的线程池/进程池里执行,避免卡住事件循环,这个行为完全是开发者主动触发的,不存在隐式的线程调度。
  4. 怎么实现真正的异步并发下载
    你有两个可选方案:
    • 替换阻塞的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())
    

最后要提醒一个常见误区:给函数加async关键字不会自动把函数变成非阻塞的。如果协程内部包含同步阻塞逻辑,又没有做额外的线程/进程隔离,运行时依然会卡住整个事件循环,和普通同步代码没有区别。


内容的提问来源于stack exchange,提问作者user422005

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.31 21:03:18