Python:asyncio讲解
问题1
若一个函数是async的,它await时,只会让出cpu,但没法说让它所在的项目中其它任务进行,对吗,也就是没法进行项目内并发(使用asyncio)。
若使用asyncio.gather()的话,则可以在项目内某个环节执行并发。
这种现象是因为一个项目除非使用threading ,否则它就是一条线顺序执行,也就是单线程1 async / await 到底“让出”的是什么?
❌ 不是“让出 CPU 给操作系统 / 线程调度”
✅ 是“让出控制权给 事件循环(event loop)”
当你写:
await some_io()发生的是:
当前 协程挂起
控制权交还给 asyncio 的事件循环
事件循环去运行同一个事件循环里已经就绪的其它协程
所以这句话:
await 只会让出 cpu,但不能让项目内其它任务进行
这句话是错的
实际上:
await 正是让项目内“其它协程”得以运行的机制
2 没用 gather,就不能并发吗?
这是一个很常见的误区。
情况 A:顺序 await(无并发)
async def main():
await task1()
await task2()
await task3()执行顺序是:
task1 全部跑完
→ task2 全部跑完
→ task3 全部跑完这里:
- 每次只有一个“顶层任务”存在
- 虽然 task1 内部可能 await
- 但你没有创建多个并发任务
情况 B:创建并发任务
用 gather
async def main():
await asyncio.gather(
task1(),
task2(),
task3()
)或 create_task
async def main():
t1 = asyncio.create_task(task1())
t2 = asyncio.create_task(task2())
t3 = asyncio.create_task(task3())
await t1
await t2
await t3现在:
三个任务都注册进事件循环
只要任何一个
await卡住(IO)事件循环就会去跑别的任务
这才是:
项目内并发
3 所以你的“单线程顺序执行”说法对吗?
结论:
asyncio 默认是 单线程 + 并发调度
不是
单线程 + 顺序执行
正确模型是:
1 个线程
1 个事件循环
N 个协程任务执行模式是:
谁遇到 await 谁让路
谁就绪谁运行
4 真正的区别图
没并发
main
└── await task1 ───────────────┐
└── await task2 ───────────────┐
└── task3并发
事件循环
├── task1 ─ await ─┐
├── task2 ─ await ─┼── 交替运行
└── task3 ─ await ─┘5 CPU 密集 vs IO 密集是分水岭
asyncio 擅长:
- 网络请求
- 磁盘 IO
- 数据库
- API 调用
- sleep / 等待
asyncio 不擅长:
- 大量计算
- 图算法
- embedding 计算
- ML 推理
因为:
协程不会抢占 CPU
只有你写 await,它才让路
如果你写:
while True:
pass整个事件循环直接“死机”
6 threading 在 asyncio 里是“外挂”
你说这句:
一个项目除非使用 threading,否则就是一条线
更准确说法是:
asyncio 本身:
- 单线程
- 多协程并发
- 非抢占式调度
如果你需要:
- 真正 CPU 并行
- 或阻塞库(比如某些图算法、老模型代码)
你要用:
asyncio.to_thread(...)或
loop.run_in_executor(...)7 精准修正版(学术级表述)
我帮你把你的话改成一段完全正确、可写论文 / 技术文档级别的版本:
在 asyncio 模型中,系统通常运行在单线程的事件循环之上。
await并非让出 CPU 给操作系统线程调度器,而是将当前协程的控制权交还给事件循环,使其能够调度并执行同一事件循环中已就绪的其它协程任务。如果协程以顺序
await的方式调用,事件循环中在任一时刻只存在一个活跃任务,从而表现为顺序执行。
只有当通过asyncio.gather()或asyncio.create_task()显式创建多个任务并将其注册到事件循环中时,才会在单线程内实现协程级并发调度。asyncio 的并发本质上是面向 I/O 的协作式并发,而非基于多线程或多进程的抢占式并行执行。对于 CPU 密集型任务,通常需要借助线程池或进程池与事件循环协同运行。
8 一句话终极总结
asyncio = 单线程内的“IO 并发调度系统”
threading / multiprocessing = 真正的“CPU 并行系统”
问题2
基于上述,是否可以这样总结:
asyncio的作用目标不是CPU,而是单个线程内部(更通俗的说,当我们运行一个项目时,其内部),控制目前由哪个环节(或者说是任务/函数)来占用该线程。
而只有使用
asyncio.gather(fun1(),fun2(),···)
或者
task = asyncio.create_task()且有多个await task时,
asyncio的使用才更有意义,即实现单线程内部的资源/CPU利用。
否则若只是一味的定义async函数,和单独的直接await函数,实际仍然是单线程顺序执行。GPT回答
asyncio 控制的是事件循环中哪个“就绪的协程任务”获得执行权,而不是操作系统层面的 CPU 线程调度。
asyncio 的核心目标不是操作系统层面的 CPU 调度,而是在单线程事件循环中,对多个协程任务进行协作式调度,决定当前由哪个“就绪任务”占用执行权。
await 的语义是将当前协程的控制权交还给事件循环,使其能够调度并执行同一事件循环中已注册的其它任务,而非创建新的并发任务。
在脚本式程序中,如果仅顺序地 await 单个协程调用,事件循环中在任一时刻只存在一个活动任务,程序整体表现为顺序执行。
只有通过 asyncio.gather() 或 asyncio.create_task() 显式创建并注册多个任务到事件循环中,才会在单线程内实现协程级并发调度。
asyncio 提供的是面向 I/O 的协作式并发,而非面向 CPU 的抢占式并行;对于 CPU 密集型计算,仍需借助线程池或进程池与事件循环协同运行。
再次总结
我刚才总结过于狭义的点在于,总结中默认asyncio的使用环境是一个普通的项目,asyncio是从头定义开始的。
而你举例如fastapi,则是我们在别人已有的asyncio框架中插入新代码,此时单纯的await函数,确实会给框架中其它需要执行的任务让权,所以此时单独的await,不使用gather或create_task也是有意义的。fastapi可以说自身也只有一个asyncio入口,它比较经典的单独的await,不使用gather或create_task也是有意义的场景应该是:
一个接口被同时多个请求时,接口内部的await,如数据库操作,就有意义了,而普通的项目则很难有这种场景。更新日志
72fda-于25785-于e61c2-于
