FastAPI 协程指南:async def 还是 def?别再踩坑了!

FastAPI 协程指南:async def 还是 def?别再踩坑了!

Published on

目录:


FastAPI 协程指南:async def 还是 def?别再踩坑了!

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同步函数则被放入外部线程池中运行,以避免阻塞主线程。

2. 最经典的“坑”:在 async 函数里执行同步阻塞

下面的代码完美复现了这个“天坑”:

# 错误示范 - 千万不要这样做!
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秒,很危险。

3. 如何做出正确的选择?async def vs def

理解了以上原理,选择就变得非常清晰了。

你应该使用 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 的强大性能。

更多阅读:并发 async / await - FastAPI