2304 字
6 分钟
PythonWebpython web
WSGI → ASGI:Python Web 从同步到异步的演进

为什么 Python Web 生态要从 WSGI 走向 ASGI?同步模型的瓶颈在哪里,async/await 解决了什么,以及这跟 Java Servlet 3.1 异步化有什么相似之处。

你用 FastAPI 写 async def 时可能觉得理所当然——但 Python Web 不是一开始就支持异步的。从 WSGI 到 ASGI 的演进,背后是一整套并发模型的升级。

Java Web 对比:Java 经历了类似的演进——Servlet 2.5(同步,一个请求一个线程)→ Servlet 3.0(异步,AsyncContext)→ Servlet 3.1(非阻塞 I/O)→ Spring WebFlux(响应式,类似 ASGI 的异步事件循环)。Python 的 WSGI→ASGI 就是在走 Java 走过的同一条路。

1. WSGI:同步时代的地基#

WSGI(Web Server Gateway Interface)是 Python Web 的第一个标准接口,诞生于 2003 年(PEP 333)。它规定了:

Python3 点击展开代码
9 lines 展开代码

WSGI 的致命局限:一切都是同步的。

  • environ 是完整的 dict ——请求体必须全部读完才能传给应用
  • return [b"..."] ——响应体必须全部生成才能返回
  • 没有 await,没有事件循环,没有异步 I/O
  • 一个请求占用一个线程(或进程),1000 个并发 = 1000 个线程

Java Web 对比:WSGI 的 environ dict 就是 Java 的 HttpServletRequest 对象,start_response 回调就是 HttpServletResponse。同步模型下,WSGI 相当于 Servlet 2.5 时代——每个请求吃一个线程。

2. 同步模型的瓶颈在哪里#

假设你的接口要做三件事:

Python3 点击展开代码
5 lines 展开代码

同步模型下,这 65ms 里线程全程占着,即使大部分时间在等 I/O。1000 个并发 → 1000 个线程 → 每个线程 8MB 栈 → 8GB 内存。

异步模型的思路:等 I/O 的时候把线程让出去干别的。

同步: [=====CPU=====][==========等DB==========][=CPU=][=======等API=======][=CPU=]
线程占着不放
异步: [=====CPU=====] 让出 → 去处理别的请求 → DB 回来了 → [=CPU=] 让出 → API 回来了 → [=CPU=]

这就是 Python async/await 做的事:在等 I/O 的间隙去处理别的请求,用更少的线程服务更多的并发

2.1 协程(coroutine)到底是什么#

上面说的"暂停、让出、恢复",在 Python 里的实现就叫协程。理解协程最好的方式,是从最普通的函数开始对比:

普通函数(子例程)

Python3 点击展开代码
4 lines 展开代码

普通函数的控制流是单向的:调用者 → 函数 → 返回。函数在执行期间不可能暂停,也不可能把控制权暂时还给调用者。

生成器 — 协程的前身

Python3 点击展开代码
8 lines 展开代码

yield 做到了普通函数做不到的事:暂停 + 恢复。调用 next() → 函数执行到 yield → 暂停,返回一个值 → 再 next() → 从上次暂停处继续。这种"可以暂停的函数"就是协程的雏形。

async/await — 真正的协程

Python3 点击展开代码
5 lines 展开代码

async def 声明的函数返回一个协程对象。调用 fetch_data("...") 不会执行函数体——它只创建了一个"待执行的配方":

Python3 代码示例
2 lines

await 是协程的"暂停点"。当执行到 await http_get(url) 时:

  1. 协程暂停
  2. 控制权还给事件循环
  3. 事件循环去处理别的协程(比如另一个请求的 fetch_data
  4. http_get 完成后,事件循环把结果送回这个协程
  5. 协程从 await 处恢复,继续执行

一句话:协程就是一个可以在等结果时主动让出控制权、之后能被恢复继续执行的函数。它不是线程——没有抢占、没有锁、没有上下文切换开销。10000 个协程可以跑在一个线程里,靠事件循环来回调度。

Java 对比:Java 的协程(Virtual Threads,JDK 21+ 正式发布)更晚才落地。在 Virtual Threads 之前,Java 靠线程池 + CompletableFuture 来做异步——本质是多线程,每个线程有独立栈。Python 的协程从 Python 3.5(2015)就是一等公民,走的事件循环 + async/await 路线,是单线程内调度。Go 的 goroutine 更接近 Java 的 Virtual Threads——用户态调度、可抢占、多核并行。

2.2 事件循环(Event Loop)的角色#

协程不能自己运行——需要一个调度器来管理所有的协程,决定"谁该执行、谁该等待"。这个调度器就是事件循环。

Python3 点击展开代码
12 lines 展开代码

事件循环的工作方式:

事件循环的队列: [fetch_data("api1"), fetch_data("api2")]
1. 取 fetch_data("api1") → 执行到 await http_get → 暂停,api1 的 I/O 发出
2. 取 fetch_data("api2") → 执行到 await http_get → 暂停,api2 的 I/O 发出
3. 队列空了,事件循环在后台等 I/O 事件
4. api1 的响应回来了 → 把 fetch_data("api1") 放回队列
5. 取 fetch_data("api1") → 从 await 处恢复,继续执行 → 完成
6. api2 的响应回来了 → 把 fetch_data("api2") 放回队列
7. 取 fetch_data("api2") → 恢复 → 完成

这就是单线程并发——没有两个协程同时在跑,但每个协程都在等 I/O 时不浪费 CPU

2.2.1 事件循环怎么知道"该恢复了"?#

你代码里写 await http_get(url) 时,底层发生的事情:

Python3 点击展开代码
3 lines 展开代码

http_get(url) 底层会创建一个 TCP socket 连接到目标服务器,然后调用操作系统的 send() 发出 HTTP 请求。之后,这个 socket 就进入"等待响应"状态。

关键机制:事件循环不是轮询,是靠操作系统的 I/O 多路复用(epoll/kqueue)。

Python3 点击展开代码
27 lines 展开代码

一句话版本

  1. 协程 await 一个 I/O 操作时,事件循环把这个协程和对应的**文件描述符(fd)**绑定,记到等待表里
  2. 所有协程都挂起后,事件循环调 epoll.wait()——操作系统把线程挂起,CPU 完全释放
  3. 当网卡收到数据 → 内核中断 → TCP 栈处理 → 内核标记该 fd 为"可读" → epoll.wait() 返回这个 fd
  4. 事件循环根据 fd 查到对应的协程 → 放回就绪队列 → 协程从 await 处恢复

所以不是"事件循环不断轮询每个协程看数据到了没"——它在等操作系统通知。10000 个协程都在等 I/O 时,事件循环只调了一次 epoll.wait(),阻塞在那,CPU 利用率为 0%。这就是为什么单线程能撑几万并发——大部分时间它在等内核通知,不消耗 CPU。

asyncio.create_taskawait 的分工

Python3 点击展开代码
7 lines 展开代码

create_task 是"让这个协程开始跑",await 是"我需要这个值,没好的话我等着"。两者分开让你可以同时启动多个 task,让它们并发执行,再逐一等待结果。

类比:事件循环是餐厅前台,协程是等菜的客人,fd 是桌号。客人坐下后告诉前台"我在 3 号桌,菜好了叫我"。前台不每隔 5 秒跑去问后厨"3 号桌的菜好了没"——他等后厨按铃(epoll 通知),然后去 3 号桌喊客人。

2.3 协程不是线程:关键区别#

线程协程
调度者操作系统(抢占式)事件循环(协作式)
切换代价高(保存/恢复寄存器、切换内核栈)低(只是 Python 函数调用)
内存占用每线程约 8MB 栈空间每协程约几 KB
并发量几百到几千几万到几十万
适合场景CPU 密集型I/O 密集型
Python 里的问题GIL 限制多线程并行GIL 不影响,单线程内调度

协作式 vs 抢占式:线程会被操作系统随时暂停(抢占),你不需要在代码里显式说"我这里可以暂停"——但代价是要用锁保护共享数据。协程是协作式的——只在 await 处主动让出,你不用加锁(因为不会有两个协程同时访问同一个变量),但你要保证不在协程里写阻塞代码(会卡住整个事件循环)。

面试常见追问:"Python 的 async 能利用多核吗?" — 不能,因为它是单线程事件循环。如果需要多核并行,用 multiprocessingconcurrent.futures.ProcessPoolExecutor 在多个进程里各跑一个事件循环。但 Web 服务通常是 I/O 密集型,单核 + 协程已经够用。

3. ASGI:异步时代的接口#

ASGI 把 WSGI 的单次"请求→响应"模型升级为可暂停、可恢复的异步协议。

Python3 点击展开代码
17 lines 展开代码

关键差异

WSGIASGI
函数签名def app(environ, start_response)async def app(scope, receive, send)
读取请求体一次性全部读入 environ["wsgi.input"]await receive() 分块读取
发送响应start_response + return [body]await send(...) 分块发送
并发模型多线程/多进程单线程事件循环 + async/await
WebSocket不支持原生支持
代表框架Flask, Django 1.xFastAPI, Starlette, Django 3+

4. FastAPI 的 async def 到底什么意思#

Python3 点击展开代码
4 lines 展开代码

async def 意味着:

  • 这个函数返回一个协程(coroutine)
  • Uvicorn 在事件循环里调度它
  • 遇到 await 时,协程暂停,事件循环去处理别的请求
  • 被等待的操作完成后,协程恢复执行

如果你写成普通 def

Python3 点击展开代码
4 lines 展开代码

FastAPI 会把同步函数扔进线程池执行,避免阻塞事件循环。但这是兜底策略——能 async 就 async。

5. ASGI 支持 WebSocket 是因为协议本身就是双向的#

WSGI 不支持 WebSocket 的根本原因:它的设计是"一个请求 → 一个响应",单向的。

ASGI 的 scope/receive/send 模型天然支持双向持久连接——因为 receive()send() 是独立的、可以反复调用的异步函数:

Python3 点击展开代码
11 lines 展开代码

6. 这一篇在整个体系中的位置#

HTTP 协议 ← 所有 Web 框架的共同地基
WSGI (2003) ← Python 同步 Web 的起点(Flask, Django)
│ Java: Servlet 2.5
ASGI (2018) ← Python 异步 Web 的新标准(FastAPI, Starlette)
│ Java: Servlet 3.1 / Spring WebFlux
FastAPI ← 基于 ASGI 的现代 Web 框架

理解这条演进路径后,async def 不再只是语法,而是整个 Python Web 并发模型升级的终点。

专题阅读

PythonWeb

这篇文章属于同一条阅读链。你可以直接在这里切换,不用再回到列表页重新找。

当前进度14 / 16

留言区

留言

欢迎纠错、补充、交流。昵称和评论内容必填;如果你愿意,也可以留下联系方式,仅站主可见。

0

正在加载评论...

0 / 2000

阅读导航

文章目录

当前阅读位置将在这里显示

0 节