Python 的 async def 为什么能同时跑几千个任务?拆开协程对象、await 与事件循环那次让出

简介: 1000 个 asyncio.sleep 并发 0.049 秒、串行 30.953 秒,相差 626 倍,全程单线程。本文用 dis 拆开 await 的 SEND/YIELD_VALUE,讲清协程对象、事件循环就绪队列与 IO 多路复用,以及为什么 time.sleep 一进协程就把整条线程卡死,附四个高频坑与三种并发模型对照

先看一组我在自己机器上跑出来的数:

import asyncio, time

async def one():
    await asyncio.sleep(0.02)

# 1000 个一起跑
await asyncio.gather(*(one() for _ in range(1000)))   # 0.049 s

# 1000 个排着队跑
for _ in range(1000):
    await one()                                       # 30.953 s

差了 626 倍。整个过程没开一个线程,没起一个进程,就是一个线程在那儿转。

很多人把这件事理解成「asyncio 比线程快」,其实不对。快的是等待被重叠了,不是代码跑得更快。要弄清这一点,得从 async def 到底造出了什么开始拆。

一、async def 定义的是协程函数,不是普通函数

import inspect

async def fetch():
    return 42

print(inspect.iscoroutinefunction(fetch))   # True
print(type(fetch))                          # <class 'function'>

函数还是那个函数对象,只是被打了个标记:CO_COROUTINE。这个标记决定了调用它的时候,解释器不会立刻执行函数体。

二、调用它,函数体一行都不会跑

这是新手最容易懵的地方:

c = fetch()
print(type(c))            # <class 'coroutine'>
print(inspect.iscoroutine(c))   # True

fetch() 返回的是一个协程对象,一串还没启动的字节码。此时函数体里哪怕写着 print("hello"),也不会输出。

直接这么写,Python 会给你一句警告:

RuntimeWarning: coroutine 'fetch' was never awaited

协程对象自己不会跑,得有人驱动它。这个人就是事件循环。CPython 里 asyncio.run() 干的第一件事,就是把最外层那个协程包装成 Task,交给循环。

顺手记一下:既然是对象,就能被存起来、放进列表、传给别的函数。这跟普通函数调用「立刻拿结果」的语义,是两种不同的东西。

三、await 让出的是控制权,不是 CPU

反汇编一个最小协程,看看 await 处生成了什么(CPython 3.13):

import dis

async def inner():
    await asyncio.sleep(0)

dis.dis(inner)

关键几条指令是 GET_AWAITABLE、SEND、YIELD_VALUE、END_SEND。名字已经把机制说透了:SEND 把值送进被等待的对象,YIELD_VALUE 把当前协程挂起并产出一个值给调用方。

所以 await 的完整语义是:

  1. 拿到后面那个对象的「可等待」版本;
  2. 把当前协程执行到一半的现场(局部变量、指令指针、栈)连同控制权一起交回去;
  3. 交回给谁?交给驱动它的 Task / 事件循环;
  4. 等到它等的那件事(IO 就绪、定时器到期)完成了,循环再把现场恢复,从 YIELD_VALUE 后面接着跑。

注意第 3 步:控制权是在 Python 代码内部交接的,没有陷入内核,没有线程切换,没有 GIL 释放。这跟 time.sleep() 那种「把整个线程交给操作系统去睡」完全不是一回事。

我更愿意把 await 读成「我先让一让,好了叫我」,而不是「等待」。它是一次主动的、有记录的让出。

四、事件循环在干什么

把它想成一个不停转的单线程调度器,手里攥着两样东西:

  • 就绪队列:现在可以往下跑的 Task;
  • IO 多路复用器:Linux 上是 epoll,Windows 上是 IOCP / select,负责盯住一堆 socket 和定时器。

循环每一轮做三件事:

  1. 从就绪队列取一个 Task,驱动它跑到下一个 await;
  2. Task 让出时,把它挂起,把它在等的那个 fd 或定时器登记到多路复用器;
  3. 队列空了就调 selector.select() 阻塞等待;哪个 IO 好了,把对应 Task 放回就绪队列。

伪代码长这样:

while True:
    while ready:
        task = ready.popleft()
        task.step()              # 跑到下一个 await / 结束
    timeout = next_timer_deadline()
    events = selector.select(timeout)
    for ev in events:
        ready.append(task_of(ev))

关键在第 3 步:整条线程只在「确实没有任何任务可跑」时才阻塞。只要有一个协程在等网络,循环就能转头去跑别的协程。1000 个 asyncio.sleep(0.02) 之所以 0.049 秒跑完,是因为它们几乎同时把定时器登记进去,然后一起等,20 毫秒后一起被唤醒。

五、为什么 CPU 密集和阻塞调用白搭

单线程调度有个硬约束:任何一个 Task 不让出,整条线程就归它了。

import time

async def blocked():
    time.sleep(0.02)          # 阻塞调用,没有 await

async def main():
    await asyncio.gather(*(blocked() for _ in range(20)))

我实测 main() 耗时 0.421 秒,正好约等于 20 × 0.02 秒的串行总和。一个协程都没并发起来。原因是 time.sleep 里没有 await,事件循环压根没拿到过控制权。

同理,纯计算(大循环、加密、图像处理)也不会让出。这类活要么扔进 asyncio.to_thread() / run_in_executor,要么直接上多进程。

一句话:asyncio 加速的是「等」,不是「算」。

六、四个最常见的坑

坑 表现 改法
协程里用了阻塞库 requests.get、time.sleep、同步数据库驱动,并发全失效 换 aiohttp / asyncpg,或 await asyncio.to_thread(...)
忘了 await 拿到 coroutine 对象当结果用,报 object ... can't be used in 'await' expression 或 RuntimeWarning 每个协程调用都检查
在普通 def 里写 await 直接 SyntaxError 想 await,函数本身必须是 async def
一个 Task 里塞 CPU 密集循环 其他 Task 集体卡住 拆成小块定期 await asyncio.sleep(0),或推到线程池

await asyncio.sleep(0) 这行看着没用,其实是「显式让出一次」的标准写法,长循环里加一句能让其他 Task 有机会跑。

七、三种并发模型怎么选

模型 调度者 适合 不适合
threading 操作系统/GIL 少量阻塞 IO、调用同步库 上千连接、CPU 密集
multiprocessing 操作系统 CPU 密集、绕开 GIL 高并发 IO、共享状态多
asyncio 用户态事件循环 上千并发网络 IO、长连接 CPU 密集、阻塞式第三方库

别把它们当竞争对手。真实项目里常见的组合是:asyncio 管网络收发,线程池兜住几个阻塞调用,多进程扛 CPU 计算。

八、一句话记住

async def 造出的是一个可暂停的函数对象;await 是它主动把现场和控制权交回事件循环的那一次让出;事件循环靠一个就绪队列加 IO 多路复用,把所有「在等」的时间叠到一起。

所以 1000 个任务能同时跑,不是因为 Python 变快了,是因为它们大部分时间根本没在跑,只是在等,而循环没陪着一起等。

(实测环境:CPython 3.13.14,Windows;文中耗时与 dis 输出均可直接复现)

相关文章
|
6天前
|
JavaScript API iOS开发
DeepSeek Harness下载、安装、配置一篇搞定(2026整理,送常用插件)
DeepSeek Harness(DSH)是DeepSeek于2026年开源的MIT协议AI Agent框架,支持插件化组装模型、工具、沙箱与UI,兼容主流大模型。本文提供免Node.js的桌面端安装包(Win/macOS),含详细图文教程及热门插件推荐。
2610 22
|
6天前
|
iOS开发 Docker MacOS
【2026实测】Docker Desktop下载、安装、汉化、配置教程(附官网安装包)
Docker Desktop 是 Docker 官方推出的桌面容器管理工具,集成引擎、CLI、图形界面与 Kubernetes,支持 Win/macOS/Linux。2026 最新版 4.88.1 强化 ARM 原生运行、镜像加速及资源可视化,附中文汉化与国内镜像配置指南。
1143 4
|
云安全 安全 网络安全
|
5天前
|
Linux 开发工具 iOS开发
Codex保姆级教程(下载+安装+登录+使用,含所有版本,2026最新)
Codex是OpenAI推出的开源AI编程智能体,本地运行,可理解整个代码库,支持自然语言指令改代码、执行命令、跑测试、修Bug,兼容Win/macOS/Linux。(239字)
|
6天前
|
安全 算法 定位技术
《高性价比人生指南》完整原版:普通人用最少的资源,过好这一生的底层逻辑~How To Live Better
📖《高性价比人生指南》是一份基于实证、拒绝焦虑的生存手册,倡导以“人生时薪”为视角,重新计算时间、金钱与精力的成本与收益。它不承诺逆袭,只教人算清账:省下的钱若耗掉时间便亏;买体验比买物品更值;睡眠是最高杠杆的健康投资;关系贵在情绪价值而非表面热络;学习重在改变行为而非自我安慰。核心理念:用低成本、高回报的微小行动,换取长期的选择权——不是成为谁,而是掌控自己人生的主动权。
|
19天前
|
人工智能 API 开发工具
DeepSeek Harness 神插件:白嫖 13 家大厂模型
`dsh-our-free-model` 是 DeepSeek Harness 神级插件:零配置、零API Key、零账号,聚合13家大厂免费模型(含DeepSeek V4.1 Flash、Kimi K3等),支持本地网关、用量看板与自动降级,真正实现“白嫖即用”。
3301 51
|
12天前
|
JavaScript 安全 前端开发
Nodejs Brotli decoder 代码执行漏洞 (CVE-2026-111497)10629
Node.js发布安全更新修复Brotli解码器高危RCE漏洞(CVE-2026-111497),建议受影响版本用户立即升级。
2128 2
|
11天前
|
人工智能 JSON Linux
【保姆级】ComfyUI部署教程(下载+安装+配置+工作流,2026最新)
ComfyUI 是一款开源节点式AI生成工具,支持图像、视频、音频及3D内容创作。通过拖拽连线组合模型加载、提示词、采样等节点,构建可复用、易修改的工作流,兼容SD1.5、SDXL、Flux、Qwen Image等主流模型。