08 - 并发模型与异步 IO
前面几章讲的是「架构层」的优化。这一章回到「应用层」:同样一台机器,为什么用对了并发模型,就能扛几倍甚至几十倍的连接? 这也是 FastAPI 主打 async 的原因。
一句话先记住:IO 密集型任务的瓶颈是「等待」,异步 IO 让 CPU 在等待时去干别的活,从而用一个线程扛住成千上万个连接。
8.1 先分清两类任务
优化并发的第一步,是判断你的任务属于哪一类:
| 类型 | 特征 | 例子 | 瓶颈在 |
|---|---|---|---|
| IO 密集型 | 大部分时间在「等」 | 查数据库、调 API、读文件 | 等待(网络/磁盘) |
| CPU 密集型 | 大部分时间在「算」 | 图片处理、加密、复杂计算 | CPU 算力 |
这个区分决定了用什么并发模型——用错了,越优化越慢。
8.2 IO 密集型:异步 IO 是王道
问题:同步阻塞时,CPU 在干等
同步处理一个请求:
|──查数据库(等 20ms)──|─算 1ms─|──调API(等 30ms)──|
↑ 这 50ms 里,CPU 基本在发呆干等!
一个线程同一时间只能处理一个请求,等待时白白浪费。
方案:异步——等待时去处理别的请求
异步(单线程 + 事件循环):
请求A: 发起查库 →(不等,切走)
请求B: 发起查库 →(不等,切走)
请求C: 发起查库 →(不等,切走)
...
请求A 的数据回来了 → 回来接着处理A
请求B 的数据回来了 → 回来接着处理B
一个线程在「大家都在等」的间隙里来回穿梭,
把等待时间利用起来,同时推进成千上万个请求。
这就是 asyncio 事件循环 的核心思想,也是 FastAPI 用 async def 的意义。
# 关键:IO 操作要用 await,才能在等待时让出控制权
@app.get("/user/{id}")
async def get_user(id: int):
user = await db.fetch_user(id) # 等待时让出,去处理别的请求
return user
致命陷阱:在
async函数里写了同步阻塞的代码(比如用了同步的数据库驱动、time.sleep、CPU 密集计算),会卡住整个事件循环,所有请求一起卡。异步的世界里,一处阻塞,全线瘫痪。
8.3 CPU 密集型:异步救不了,要靠多进程
异步只对「等待」有用。如果任务是纯计算(一直占着 CPU),异步没有任何帮助——因为根本没有「等待的间隙」可以利用。
而且 Python 有 GIL(全局解释器锁):同一进程内,多线程也无法真正并行跑 CPU 计算。
CPU 密集型任务的正确姿势:
· 多进程(multiprocessing)→ 绕开 GIL,真正用满多核
· 丢给专门的任务队列 / Worker 处理
· 甚至交给专门的服务(如用 C/Rust 写的计算服务)
| 任务类型 | 正确并发方案 |
|---|---|
| IO 密集(大部分 Web 接口) | 异步 IO(asyncio / async def) |
| CPU 密集 | 多进程 / 独立 Worker |
| 混合 | 主流程异步 + CPU 部分丢进程池/队列 |
8.4 部署层面:多进程 + 异步结合
生产环境部署 FastAPI,通常是多个 worker 进程,每个进程内跑一个异步事件循环:
Gunicorn / Uvicorn
┌──────────────────────────────┐
│ Worker 1(异步事件循环) │ ← 每个进程用一个 CPU 核
│ Worker 2(异步事件循环) │
│ Worker 3(异步事件循环) │
│ Worker 4(异步事件循环) │
└──────────────────────────────┘
多进程用满多核 + 每进程异步扛大量并发连接
- 进程数:一般设为 CPU 核数(
worker = CPU 核数起步) - 每个进程内:异步事件循环处理海量 IO 密集请求
这就把「多进程用满 CPU」和「异步扛住海量连接」两个优势结合起来了。
8.5 一张图选对并发模型
你的任务是什么?
│
┌────┴────┐
▼ ▼
IO 密集 CPU 密集
(等得多) (算得多)
│ │
▼ ▼
异步 IO 多进程 / 独立 Worker
async/await 绕开 GIL 用满多核
│
▼
⚠️ 别在 async 里写阻塞代码!
本章小结
| 你要记住 | 要点 |
|---|---|
| 先分类 | IO 密集(等待多)vs CPU 密集(计算多) |
| IO 密集 | 用异步 IO,等待时去处理别的请求,单线程扛海量连接 |
| 异步陷阱 | async 里绝不能有同步阻塞代码,否则卡死整个循环 |
| CPU 密集 | 异步没用,靠多进程绕开 GIL |
| 部署 | 多进程(用满核)+ 每进程异步(扛连接) |
下一章预告:理论讲完了,我们把前八章的所有手段串起来,看一个真实接口「从 200 QPS 优化到 2 万 QPS」的完整推演。
上一章 ← 07 - 限流、降级、熔断 | 下一章 → 09 - 一个高并发接口的优化实录