生命周期事件
本教程共 50 篇 · 第 43 篇 · 更新于 2026-08-12 · 约 6 分钟阅读
本节目标:学会在 FastAPI 应用启动和关闭时执行初始化与清理代码,并理解新版 lifespan 写法为何取代了旧的 on_event 装饰器。
写接口时,有些代码不需要每次请求都跑。比如连数据库的连接池、加载一个很大的机器学习模型、读取一份共享的配置。这些东西只需要在应用启动时准备好一次,然后在应用关闭时释放掉。FastAPI 把这类时机称为「生命周期」。
43-1 什么时候需要生命周期逻辑
设想你有一个接口要调用一个机器学习模型。这个模型会被所有请求共用,不是每个请求单独加载一份。加载模型很慢,要从磁盘读很多数据。
如果你把加载写在了文件最顶层,那么哪怕只是跑一个自动化测试,也会先把模型加载一遍,测试变得奇慢。更好的做法是:只在应用真正开始接收请求之前,才去加载模型。应用关闭时再把模型卸载,释放显存和内存。
这类需求都适合放在生命周期里:
- 启动时建立数据库连接池,关闭时关闭它
- 启动时预加载共享数据到内存,关闭时清空
- 启动时连接消息队列,关闭时断开
43-2 推荐写法:lifespan 上下文管理器
新版 FastAPI 推荐用 lifespan 参数来管理启动和关闭逻辑。它本质是一个「异步上下文管理器」,用 yield 把代码分成两段:yield 之前的代码在启动前执行,yield 之后的代码在关闭前执行。
from contextlib import asynccontextmanager
from fastapi import FastAPI
# 模拟一个很重的模型加载过程
def load_model():
return {"model_a": "已加载的假模型"}
# 模拟卸载模型
def unload_model(models):
models.clear()
@asynccontextmanager
async def lifespan(app: FastAPI):
# 启动阶段:在接收请求之前执行
models = load_model()
print("应用已启动,模型加载完成")
yield
# 关闭阶段:在停止接收请求之后执行
unload_model(models)
print("应用已关闭,模型已释放")
app = FastAPI(lifespan=lifespan)
@app.get("/")
async def root():
return {"msg": "hello"}
重点看 lifespan 函数。它用 @asynccontextmanager 装饰,内部是一个 async 函数,中间有一个 yield。yield 之前的代码在应用「开始接收请求之前」运行一次;yield 之后的代码在应用「停止接收请求之后」运行一次。
yield 后面不接任何值也没关系,它的作用只是把上下文一分为二。最后我们把 lifespan= lifespan 传给 FastAPI(...),FastAPI 就会在合适的时机调用它。
Tip上下文管理器的概念你其实用过:Python 里的
with open("file.txt") as f:就是上下文管理器。它在进入with块前打开文件,退出后自动关闭。asynccontextmanager是异步版的同一个思想。
43-3 启动代码与关闭代码怎么分工
回到模型那个例子。我们把「加载」放在 yield 之前,「卸载」放在 yield 之后,二者共享同一个 models 变量,因为它们在同一个函数作用域里,启动和关闭两段之间不需要用全局变量传来传去。不过请求处理函数如果也要访问这些资源,仍需要把它存到模块级容器(或通过依赖注入暴露),lifespan 内部的共享作用域只覆盖启动和关闭两段。
from contextlib import asynccontextmanager
from fastapi import FastAPI
ml_models = {}
@asynccontextmanager
async def lifespan(app: FastAPI):
# 启动:把模型放进共享字典
ml_models["model_a"] = "fake_model_object"
yield
# 关闭:清空字典,释放资源
ml_models.clear()
app = FastAPI(lifespan=lifespan)
@app.get("/predict")
async def predict():
# 请求处理函数里可以直接用 ml_models
return {"model": ml_models.get("model_a")}
这样模型在应用启动后一直留在内存里,每个请求都能直接用,应用关掉时才清空。
43-4 旧写法 on_event 已弃用
曾经 FastAPI 提供 @app.on_event("startup") 和 @app.on_event("shutdown") 两个装饰器来写启动和关闭逻辑。下面是旧写法示例,你可能在老教程里见过:
from fastapi import FastAPI
items = {}
@app.on_event("startup")
async def on_startup():
# 启动时填充一个内存字典
items["key"] = "value"
@app.on_event("shutdown")
def on_shutdown():
# 关闭时把数据写入日志文件
with open("log.txt", mode="a") as f:
f.write("Application shutdown\n")
Warning这种
@app.on_event写法已经被官方标记为弃用。如果你给应用传了lifespan参数,那么startup和shutdown事件处理函数将不再被调用。二者只能选一种,不能混用。新项目请一律使用上面讲的lifespan。
旧写法有两个明显缺点。第一,启动和关闭的代码被拆成两个函数,若它们要共享状态,就得靠全局变量,容易出错。第二,FastAPI 官方希望统一到 ASGI 标准的 lifespan 协议上,方便和其他工具协作。所以除非你在维护老代码,否则不要再写 on_event。
Note关于阻塞 I/O:旧文档示例里关闭时写文件用了普通
def而不是async def,因为open()不是异步函数。如果你在 lifespan 里做文件读写、同步网络请求这类阻塞操作,应当用普通def函数或在线程池里执行,避免卡住事件循环。
43-5 技术细节与子应用
在底层,这套机制遵循 ASGI 规范的「Lifespan 协议」,定义了 startup 和 shutdown 两个事件。FastAPI 的 lifespan 最终也是映射到这两个事件上。
还要记住一点:生命周期事件只在主应用上触发,不会在通过 app.mount() 挂载的子应用(Sub Applications / Mounts)上触发。如果你用了挂载,要把需要的初始化逻辑放进主应用的 lifespan 里。
另外要留意 lifespan 与多进程部署的关系。用 fastapi run --workers 4 起四个工作进程时,lifespan 会在每个进程里各跑一遍,而不是全局只跑一次。这意味着预加载的模型、建立的连接池都是每进程一份,内存占用要按进程数乘。更要紧的是,像「启动时建表」「启动时写一条初始化记录」这类只应执行一次的操作,放在 lifespan 里会被重复执行。这类任务更适合抽成独立脚本,在部署流程中单独运行一次。
43-6 小结
启动和关闭逻辑用 lifespan 上下文管理器最干净。yield 之前放启动代码,之后放关闭代码,二者共享变量。旧的 @app.on_event("startup"/"shutdown") 已弃用,不要在新代码里使用。把重资源的准备和释放放进 lifespan,既提升性能又方便测试。