首页 / 知识库 / python服务端进阶 / 服务端高并发优化

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 - 一个高并发接口的优化实录