为什么需要 asyncio
先看一个普通同步程序:
import time
def fetch(name, delay):
print(f"{name}: send request")
time.sleep(delay)
print(f"{name}: response")
fetch("profile", 0.4)
fetch("orders", 0.2)
fetch("recommendations", 0.3)这段代码的慢点在等待。第一个请求没回来,第二个请求就不能开始;第二个没回来,第三个也只能排队。线程大部分时间没在做计算,只是在等结果。
asyncio 适合这种场景:手上有很多 I/O 工作,比如请求接口、读写 socket、等子进程输出。每件事本身算得不多,主要时间都花在等外部响应上。我们想在一个线程里把这些等待时间错开,而不是等完一个再做下一个。
并发不是并行
| 概念 | 重点 | 在 asyncio 里的含义 |
|---|---|---|
| 并发 | 多件事在一段时间内交替往前走 | 一个 Task 等 I/O 时,事件循环去跑另一个 Task |
| 并行 | 多件事在同一时刻真的同时运行 | 需要多线程、多进程或多机器 |
| 阻塞 | 当前执行流等到结果回来前不让出控制权 | time.sleep()、同步 socket、同步数据库调用 |
| 非阻塞 | 操作暂时没结果时先返回,稍后再通知 | selector 监听 socket 就绪,Future 表示未来结果 |
asyncio 默认不会让 Python 代码同时跑在多个 CPU 核上。它做的是另一件事:一个任务在等网络时,别让线程闲着,先去处理别的任务。
一个请求聚合场景
假设你要构造用户首页,需要同时拿用户资料、订单、推荐结果:
同步写法总耗时接近 0.4 + 0.2 + 0.3 = 0.9s。如果三个请求彼此独立,理想耗时接近最慢的 0.4s:
asyncio 要做的就是这件事:把这些等待时间重叠起来。三个请求一起发出去,谁先回来先处理谁,最后再把结果汇总。
asyncio 适合什么
适合:
- 高并发网络客户端,例如批量调用 HTTP API。
- 网络服务,例如 TCP 服务、WebSocket 服务和异步 Web 框架内部。
- 需要排队、限流、超时、取消的 I/O 工作流。
- 子进程、管道、socket 等可以由事件循环管理的 I/O。
它不适合直接处理:
- 大量 CPU 密集计算。可以配合
asyncio.to_thread()、run_in_executor()、多进程或专门的计算框架。 - 已经被同步库锁死的调用链。同步数据库驱动放进
async def里不会自动变成异步。 - 需要系统强行打断任务的场景。asyncio 里的协程只有走到
await,才会把执行机会让出来。
为什么它适合 I/O 程序
asyncio 的调度方式比较克制。它不会在你代码写到一半时突然停下来,只有遇到 await 才会暂停当前协程。比如:
async def handle():
state["step"] = "start"
await fetch_remote_data()
state["step"] = "done"读这段代码时,你可以把注意力放在 await 上。await 前面的赋值会连续执行完;到了 await fetch_remote_data(),当前协程可能暂停,事件循环会去跑别的任务;等远端数据回来,再从下一行继续。
这个设计让暂停位置很清楚:主要看 await 在哪里。但它也有代价。如果你在协程里写了很长的 CPU 循环,中间一直没有 await,事件循环就没机会处理别的任务:
async def bad():
total = 0
for i in range(100_000_000):
total += i
return total这类代码要么拆小并主动让出,要么放到线程池、进程池或专门的计算框架里。asyncio 更适合等待很多、计算不重的程序。
小练习
运行:
python3 examples/asyncio_demos/02_concurrent_sleep.py观察输出顺序和总耗时。然后把 asyncio.gather(...) 改成三个连续的 await fetch(...),比较总耗时。