
FastAPI 协程指南:async def 还是 def?别再踩坑了!
- Published on
目录:
async def
还是 def
?别再踩坑了!
FastAPI 协程指南:FastAPI 之所以如此“Fast”,很大程度上归功于其底层的异步支持。然而,如果不能正确理解和使用协程,不仅无法发挥其性能优势,反而可能挖下大坑。如果在协程中使用高IO等同步操作,会直接阻塞 FastAPI 主线程。
下面,我们将深入探讨 FastAPI 的并发模型,并明确指出何时该用 async def
,何时又该退回到普通的 def
。
1. FastAPI 的并发模型:两条腿走路
FastAPI 非常灵活,它能同时处理两种类型的路径操作函数(也就是我们常说的端点或视图函数):
异步函数 (
async def
): 这是 FastAPI 的“主赛道”。这类函数运行在 Python 的asyncio
事件循环中。当遇到await
关键词(例如,等待数据库查询、API 请求返回)时,它会“暂停”当前任务,让事件循环去处理其他请求,从而实现极高的并发性能。关键点:async def
函数运行在主线程的事件循环里。同步函数 (
def
): 当你使用普通的def
定义端点时,FastAPI 也不会坐视不理。它会将这个同步函数放在一个独立的线程池中去执行。这样做是为了防止同步函数中的耗时操作(比如 CPU 密集型计算或不支持asyncio
的阻塞 I/O)阻塞主线程的事件循环。
一句话总结:
FastAPI定义的
async def
协程函数在主线程事件循环中运行,而def
同步函数则被放入外部线程池中运行,以避免阻塞主线程。
async
函数里执行同步阻塞
2. 最经典的“坑”:在 下面的代码完美复现了这个“天坑”:
# 错误示范 - 千万不要这样做!
import time
from fastapi import FastAPI
app = FastAPI()
@app.get("/square/{num}")
async def square(num: int):
# time.sleep() 是一个同步的、阻塞的函数
# 它会冻结整个事件循环长达10秒
time.sleep(10)
return {"num": num, "q": str(num**2)}
问题分析:
async def
关键字并不会神奇地将函数体内所有代码都变成异步非阻塞的。在这个例子中,time.sleep(10)
是一个同步阻塞调用。当事件循环执行到这一行时,它会被强制“暂停”,无法处理任何其他进入的请求,直到 10 秒结束后才能继续。
这会导致整个服务“假死”10秒,所有其他用户请求都会被挂起,完全违背了使用异步框架的初衷。
图片中的注释一针见血:
像这样在异步函数中用同步耗时函数,会直接阻塞主线程10秒,很危险。
async def
vs def
3. 如何做出正确的选择?理解了以上原理,选择就变得非常清晰了。
async def
的场景:
你应该使用 当你需要执行 I/O 密集型任务,并且你所使用的库支持 asyncio
时。
- 网络请求: 使用
httpx
替代requests
。 - 数据库操作: 使用
asyncpg
(PostgreSQL),motor
(MongoDB),databases
等异步库。 - 等待: 使用
asyncio.sleep()
替代time.sleep()
。
正确示例(非阻塞等待):
import asyncio
from fastapi import FastAPI
app = FastAPI()
@app.get("/wait-non-blocking")
async def wait_non_blocking():
# 正确做法:使用 asyncio.sleep()
# 在等待期间,事件循环可以去处理其他请求
await asyncio.sleep(10)
return {"message": "I waited 10 seconds without blocking the server!"}
def
的场景:
你应该使用 当你需要执行 CPU 密集型任务,或者必须使用一个不支持 asyncio
的阻塞库时。
- 复杂计算: 图像处理、数据分析、机器学习模型推理等。
- 使用传统阻塞库: 如一个老的数据库驱动、文件处理库等。
正确示例(把阻塞任务交给线程池):
import time
from fastapi import FastAPI
app = FastAPI()
@app.get("/process-blocking")
def process_blocking():
# 正确做法:用 def 定义这个端点
# FastAPI 会自动把它放到线程池里运行,不会阻塞主线程
time.sleep(10) # 模拟一个耗时的CPU密集型或阻塞I/O任务
return {"message": "The server was responsive while I was processing!"}
4. 决策总结
为了方便您快速决策,可以参考下表:
任务类型 | 推荐定义方式 | 核心原因 |
---|---|---|
I/O密集型 (数据库, 外部API调用) 且有异步库支持 | async def | 利用 await 实现高并发,最大化性能。 |
CPU密集型 (复杂数学计算, 图像处理) | def | 将计算任务扔到线程池,避免主线程被“计算”卡死。 |
依赖只支持同步的库 | def | 隔离阻塞调用,保护事件循环的流畅运行。 |
内部没有任何耗时或I/O操作 | 两者皆可 | 性能差异可忽略。但遵循FastAPI范式,推荐 async def 。 |
最终建议:深入理解你的依赖库和代码逻辑是关键。始终问自己一个问题:“这段代码会阻塞吗?” 如果答案是肯定的,而且没有异步替代方案,那么就果断地将它包裹在 def
路径操作函数中。这样,你才能真正驾驭 FastAPI 的强大性能。