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 会被占住,并发能力反而崩掉。此时要么换成异步库(httpxaiosqlite 等),要么把端点写成普通的 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 并非全程锁死,有几类情况会释放或绕开:

  1. 阻塞 IO:线程执行网络、磁盘等系统调用时,CPython 会释放 GIL,其他线程可以继续执行
  2. C 扩展主动释放:NumPy、hashlib、zlib 等 C 实现的重计算库在进入 C 代码前会释放 GIL,计算期间不持锁
  3. 定时切换:旧版 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 核心利用率问题,两者解决的是不同的问题,通常搭配使用。