FastAPI 的 async/await 与 GIL:并发和并行到底是怎么回事
1. 什么是 GIL
Global Interpreter Lock(全局解释器锁,简称 GIL)是 CPython 解释器中的一把互斥锁。它的规则很直接:同一时刻,一个进程内只允许一个线程执行 Python 字节码。
需要注意几个要点:
- 一个进程仍然可以创建多个线程,但在任一瞬间,只有持有 GIL 的那个线程在执行 Python 字节码,其余线程只能等待
- GIL 是 CPython 的实现细节,不是 Python 语言规范的一部分(Jython、IronPython 没有 GIL)
- GIL 存在的主要原因是简化 CPython 的内存管理(引用计数的线程安全),而非刻意限制性能
也就是说,多线程在 CPU 密集型的 Python 代码面前,往往无法带来吞吐提升,甚至因为线程切换和锁竞争而变得更慢。
2. FastAPI 的 async/await:协作式调度
FastAPI 的 async/await 建立在 asyncio 之上,使用协作式调度(cooperative scheduling):
- coroutine 在
await处主动把 Event Loop 的控制权交回来,让其他协程得以运行 - 调度的前提是“大家都自觉”:只要有一个协程长时间不让出控制权(比如执行了阻塞的同步调用),整个 Event Loop 都会被卡住
@app.get("/items")
async def list_items():
# await 处主动让出控制权,等待 IO 期间 Event Loop 可以处理其他请求
data = await fetch_from_db()
return data
这也解释了一个常见误区:把 async 加上并不等于高性能。如果在 async def 里调用了阻塞的同步函数(如 requests.get、同步的 DB 驱动),Event Loop 会被占住,并发能力反而崩掉。此时要么换成异步库(httpx、aiosqlite 等),要么把端点写成普通的 def,让 FastAPI 自动丢到线程池里执行。
3. 并发 ≠ 并行:一张表分清
| 概念 | 本质 | FastAPI 中的体现 | 是否受 GIL 限制 |
|---|---|---|---|
| 并发(concurrency) | 交替处理多个任务 | 单个 Event Loop 上的多个 coroutine | 是(都在一个线程里) |
| 并行(parallelism) | 同一时刻真正同时执行 | 多 worker / 多进程各跑一个 Event Loop | 每个 worker 独立进程,各有自己的 GIL |
FastAPI 的 async 可以近似看成“单 Event Loop 线程上的协作式并发“:它擅长的是 IO 密集型场景——成千上万个连接在同一根线程上轮流等待、轮流处理,线程在等 IO 时不占 CPU,切换成本远低于线程上下文切换。
而真正的多线程多核并行,取决于线程执行的代码是否能够绕过/释放 GIL,或者直接使用多进程、多 worker。
4. 什么时候 GIL 会被释放
GIL 并非全程锁死,有几类情况会释放或绕开:
- 阻塞 IO:线程执行网络、磁盘等系统调用时,CPython 会释放 GIL,其他线程可以继续执行
- C 扩展主动释放:NumPy、hashlib、zlib 等 C 实现的重计算库在进入 C 代码前会释放 GIL,计算期间不持锁
- 定时切换:旧版 CPython 每隔 5ms(
sys.getswitchinterval())强制切换一次 GIL 的持有者
flowchart TD
A[Python 进程] --> B[线程 1 持有 GIL]
A --> C[线程 2 等待 GIL]
B -->|遇到 IO / C 扩展释放| D[释放 GIL]
D --> C
C --> E[线程 2 执行字节码]
因此,多线程对 IO 密集型任务仍然有效(等待期间锁是放开的),但对纯 Python 计算密集型任务无能为力。
5. FastAPI 场景下的实践建议
| 任务类型 | 推荐方案 | 原因 |
|---|---|---|
| IO 密集(DB、HTTP 调用) | async def + 异步库 |
Event Loop 协作并发,切换开销极低 |
| IO 密集但只有同步库 | 普通 def 端点 |
FastAPI 自动放入线程池,阻塞不影响 Event Loop |
| CPU 密集(纯 Python 计算) | 多 worker / 多进程(或 run_in_executor 进程池) |
绕开单进程 GIL,利用多核 |
| CPU 密集但可下沉到 C | NumPy 等释放 GIL 的库 | C 计算期间不持锁,可与 Event Loop 并存 |
部署层面的常用组合:
uvicorn app:app --workers 4
# 或
gunicorn app:app -k uvicorn.workers.UvicornWorker -w 4
每个 worker 是独立进程,各自拥有独立的 GIL 和 Event Loop,天然实现多核并行。这也是前面压测结论能成立的基础——4 个 uvicorn worker 才能在多核机器上把 QPS 撑起来。
6. 小结
- GIL 限制的是“同一进程内同时执行 Python 字节码的线程数”,不限制进程数
- FastAPI 的
async/await是单线程协作式并发,靠“在await处主动让出”来吞吐 IO,不是并行 - 想要真正的多核利用:让重计算走释放 GIL 的 C 扩展,或者直接上多进程、多 worker
- 一句话:async 解决的是 IO 等待的效率问题,多 worker 解决的是 CPU 核心利用率问题,两者解决的是不同的问题,通常搭配使用。