FastAPI 中 ASGI 和异步 async/await 是什么关系?
简化版
ASGI 是 Python 异步 Web 服务的接口规范,FastAPI 运行在 ASGI 之上,因此可以用 async/await 编写非阻塞接口。async/await 只有在内部调用异步 I/O 时才真正发挥作用,如果里面调用同步阻塞代码,仍然会拖慢事件循环。
详细版
ASGI 规定了应用和服务器之间如何用异步方式通信,Uvicorn、Hypercorn 这类服务器负责运行 ASGI 应用。FastAPI 基于 Starlette,而 Starlette 是 ASGI 框架,所以 FastAPI 天然支持异步请求处理。
在 FastAPI 中:
async def路由会在事件循环中执行;def路由通常会被放到线程池中执行,避免阻塞事件循环;- 异步接口内部应该使用异步库,例如
httpx.AsyncClient、异步数据库驱动、异步 Redis 客户端; - 如果在
async def里调用time.sleep()、同步 requests、同步数据库查询,就会阻塞事件循环。
正确理解是:ASGI 提供异步运行环境,async/await 是编写异步协程的语法,FastAPI 把二者结合起来用于 Web API 开发。
完整版教学
一、先理解 WSGI 和 ASGI 的区别
WSGI 是 Python 传统 Web 生态里的同步接口规范,Flask、Django 早期主要运行在 WSGI 模型下。它的模型简单直接:服务器收到请求,调用应用对象,应用返回响应。这个模型非常成熟,但它天然是同步调用模型。
ASGI 可以看作 WSGI 的异步升级版。它不仅支持 HTTP,还支持 WebSocket、长连接、后台事件等更现代的通信方式。ASGI 应用是一个异步可调用对象,服务器和应用之间通过事件消息通信。
FastAPI 基于 Starlette,Starlette 是 ASGI 框架,所以 FastAPI 能自然处理异步请求、WebSocket、流式响应等场景。
二、async/await 到底做了什么
async def 定义的是协程函数,调用它不会立刻执行完整逻辑,而是返回一个协程对象。await 表示当前协程遇到了一个可以等待的异步操作,比如异步网络请求、异步数据库查询。等待期间,事件循环可以去调度其他协程。
举个直观例子:餐厅只有一个服务员。如果服务员给一桌客人下单后站在厨房门口一直等菜,这就是同步阻塞。如果服务员下单后去服务其他桌,等厨房通知菜好了再回来,这就是异步 I/O 的感觉。
在 FastAPI 里,一个请求等待数据库返回时,如果使用异步驱动,事件循环可以继续处理其他请求。这样同样的线程资源可以支撑更多并发连接。
三、FastAPI 如何处理 async def 和 def
FastAPI 支持两种路由函数:
@app.get("/async")
async def async_api():
return {"mode": "async"}
@app.get("/sync")
def sync_api():
return {"mode": "sync"}
async def 会作为协程在事件循环中运行。它适合内部使用异步 I/O 的接口,例如:
import httpx
@app.get("/remote")
async def call_remote():
async with httpx.AsyncClient() as client:
resp = await client.get("https://example.com")
return {"status": resp.status_code}
普通 def 函数通常会被 FastAPI 放入线程池执行。这样即便里面有同步阻塞调用,也不至于直接卡住事件循环。对于仍然使用同步 ORM 或同步 SDK 的项目,def 路由反而可能更安全。
四、为什么 async def 里不能随便调用同步阻塞代码
事件循环的特点是“协作式调度”。协程必须在遇到 await 时主动交出控制权。如果你在 async def 中写:
import time
@app.get("/bad")
async def bad_api():
time.sleep(3)
return {"ok": True}
这 3 秒里事件循环被同步阻塞,其他协程没有机会运行。并发请求一多,整个服务就会出现延迟升高。
类似的问题还包括:在异步函数里使用 requests.get()、同步数据库客户端、大量 CPU 循环、同步文件大读写等。它们都会让 async 失去意义。
五、什么时候用 async,什么时候不用
如果接口主要调用异步数据库、异步 HTTP、异步缓存、异步消息队列,那么适合用 async def。如果你的项目依赖同步 SQLAlchemy session、同步 requests、同步第三方 SDK,可以先用普通 def,让 FastAPI 把它放到线程池。
CPU 密集型任务不要寄希望于 async。比如图片压缩、视频处理、大模型推理、复杂加密计算,这些任务会占用 CPU,应该放到进程池、Celery/RQ 任务队列,或者拆到独立计算服务。
六、面试表达的关键点
面试官问 ASGI 和 async 的关系,真正考的是你是否理解异步并发的边界。好的回答应包含三点:FastAPI 运行在 ASGI 上;async/await 让 I/O 等待期间释放事件循环;同步阻塞代码会破坏异步收益。
还可以补一句:异步不是为了让单个请求一定更快,而是为了在 I/O 等待多的场景下提高整体吞吐和资源利用率。
七、常见误区与追问
| 写法 | 适合场景 | 风险 |
|---|---|---|
async def + 异步 I/O | httpx.AsyncClient、AsyncSession、异步 Redis | 依赖库不支持异步时收益消失 |
普通 def + 同步 I/O | 同步 ORM、同步 SDK、遗留代码 | 依赖线程池容量,阻塞多会排队 |
async def + 同步阻塞 | 不推荐 | 会卡住事件循环,影响同进程其他协程 |
1 个事件循环处理多个协程:
t=0ms 请求A await 数据库
t=1ms 事件循环切到请求B
t=2ms 请求B await HTTP
t=200ms 请求A I/O 返回后继续执行
易错点:
async是协作式并发,只有遇到真正可等待的 I/O 才会让出执行权。
- 误区:把路由改成
async def就一定提升性能。 如果里面调用requests、同步数据库驱动、time.sleep()或 CPU 大循环,事件循环仍然会被阻塞。 - 误区:ASGI 等于 FastAPI 独有能力。 ASGI 是 Python 异步 Web 规范,Uvicorn、Hypercorn 是常见 ASGI 服务器,FastAPI 是运行在 ASGI 生态上的框架。
- 误区:异步会让单个数据库查询更快。 异步通常提升的是等待期间的并发处理能力,不会缩短数据库实际执行 SQL 的时间。
- 追问:FastAPI 中普通
def路由怎么处理? FastAPI 会把同步路径操作放到线程池中执行,避免直接阻塞事件循环,但线程池也有容量和调度成本。 - 追问:WebSocket 为什么更适合 ASGI? WebSocket 是长连接和事件驱动通信,ASGI 的双向消息模型比传统同步 WSGI 更自然。
- 追问:怎么判断线上是否被同步阻塞拖慢? 观察事件循环延迟、请求耗时分布、worker CPU、线程池排队和慢调用日志;如果少量慢同步调用导致大量请求一起变慢,通常就是阻塞信号。
八、加强记忆
记住这条线:ASGI 是异步 Web 规范,Uvicorn 是常见 ASGI 服务器,FastAPI 是 ASGI 应用,async/await 是写异步逻辑的语法。异步收益来自“等待 I/O 时不占着事件循环”,不是来自把所有函数都改成 async def。