FastAPI 后台任务(BackgroundTasks)详解:在响应返回之后执行耗时操作
【免费下载链接】fastapiFastAPI framework, high performance, easy to learn, fast to code, ready for production项目地址: https://gitcode.com/GitHub_Trending/fa/fastapi
FastAPI 内置的BackgroundTasks允许你定义在响应返回给客户端之后才执行的后台任务,非常适合发送邮件通知、写日志、处理上传文件等「客户端不必等待完成」的操作。本文基于 FastAPI 官方教程(见 docs/en/docs/tutorial/background-tasks.md 与法语版 docs/fr/docs/tutorial/background-tasks.md),结合本仓库的源码实现(fastapi/background.py、fastapi/dependencies/utils.py、fastapi/routing.py)与配套测试,深入讲解其用法、依赖注入机制、底层原理及其适用边界,读完后你可以直接在生产项目中用最少代码实现可靠的「返回即处理」异步化任务。
为什么需要后台任务
Web 应用中有些操作在请求完成后仍需继续执行,但客户端并不关心其最终结果,也不该被它阻塞。典型场景包括:
- 发送邮件通知:连接邮件服务器并发送邮件通常耗时数秒,返回响应本身并不需要等待发送结果。可先返回响应,把发信放到后台。
- 处理数据:例如收到一个需要慢速处理的大文件时,可以先返回
202 Accepted(表示请求已被接受、正在处理),再在后台慢慢处理。
把这些操作放到后台任务里,能显著缩短客户端感知到的接口延迟,同时保持接口返回结构不变。
使用BackgroundTasks三步走
1. 导入并声明参数
首先从fastapi导入BackgroundTasks,然后在路径操作函数中声明一个类型为BackgroundTasks的参数:
from fastapi import BackgroundTasks, FastAPI app = FastAPI() def write_notification(email: str, message=""): with open("log.txt", mode="w") as email_file: content = f"notification for {email}: {message}" email_file.write(content) @app.post("/send-notification/{email}") async def send_notification(email: str, background_tasks: BackgroundTasks): background_tasks.add_task(write_notification, email, message="some notification") return {"message": "Notification sent in the background"}完整示例见 docs_src/background_tasks/tutorial001_py310.py。这里FastAPI 会自动创建BackgroundTasks对象并通过该参数注入,你无需手动实例化,正如它处理Request对象那样。
2. 定义任务函数
后台任务本身就是一个普通的 Python 函数,可以接收任意参数。它既可以是async def异步函数,也可以是普通def同步函数,FastAPI 都会以正确方式执行它:
- 同步
def任务函数会被放入线程池执行(避免阻塞事件循环); async def任务函数会以协程方式直接调度。
上面的write_notification因为只做文件写入(不涉及async/await),就定义成了普通def。它模拟了一次邮件发送:把通知内容写入log.txt。
3. 用.add_task()添加任务
在路径操作函数内部,调用后台任务对象的.add_task()方法注册任务。.add_task()接受三类参数:
- 任务函数:要在后台执行的函数,如
write_notification; - 位置参数序列:按顺序传给任务函数,如
email; - 关键字参数:如
message="some notification"。
响应会立即返回{"message": "Notification sent in the background"},而write_notification在响应发送之后才执行。
后台任务如何与依赖注入协作
BackgroundTasks不仅能用在路径操作函数中,还能用在依赖(dependable)甚至子依赖中。FastAPI 会识别各层声明,复用同一个后台任务对象,把所有层添加的任务合并起来,统一在响应返回后依次执行:
from typing import Annotated from fastapi import BackgroundTasks, Depends, FastAPI app = FastAPI() def write_log(message: str): with open("log.txt", mode="a") as log: log.write(message) def get_query(background_tasks: BackgroundTasks, q: str | None = None): if q: message = f"found query: {q}\n" background_tasks.add_task(write_log, message) return q @app.post("/send-notification/{email}") async def send_notification( email: str, background_tasks: BackgroundTasks, q: Annotated[str, Depends(get_query)] ): message = f"message to {email}\n" background_tasks.add_task(write_log, message) return {"message": "Message sent"}完整示例见 docs_src/background_tasks/tutorial002_an_py310.py(使用Annotated标注),等价的不含Annotated的写法见 docs_src/background_tasks/tutorial002_py310.py。
在该示例中:
- 依赖函数
get_query声明了BackgroundTasks参数——如果请求带查询参数q,它会先注册一条写日志的后台任务; - 路径操作函数
send_notification又添加了另一条任务,写入email路径参数对应内容; - 两条任务会写入同一个
log.txt,且都发生在响应已发送之后,日志顺序即为添加顺序(依赖层先加、路径函数后加)。
这一行为在仓库配套测试中有完整验证,参见 tests/test_tutorial/test_background_tasks/test_tutorial001.py:测试先请求/send-notification/foo@example.com,断言返回200与 JSON 内容,再检查log.txt中确实写入了notification for foo@example.com: some notification。测试通过验证了「响应返回后任务确实执行、写入结果落盘」的完整链路。
底层原理:FastAPI 如何识别并调度后台任务
要理解「为何声明类型参数即自动注入、各层任务为何能合并」,可以回到本仓库的核心源码:
参数识别:在 fastapi/dependencies/utils.py 的
add_non_field_param_to_dependency()中,FastAPI 会检查参数类型注解:凡被判定为StarletteBackgroundTasks(含 FastAPI 中继承它的BackgroundTasks)的参数,都会被记录到dependant.background_tasks_param_name,而不会被当作普通的查询/请求体参数处理。对象注入与跨层共享:在 fastapi/dependencies/utils.py 的
solve_dependencies()中,依赖求解是递归进行的——先求解子依赖,再把子依赖返回的background_tasks透传给上一层(代码中background_tasks = solved_result.background_tasks)。因此,只要某一层存在声明BackgroundTasks的节点且对象尚不存在,就会background_tasks = BackgroundTasks()创建一次,此后所有层共享这一个实例,任务自然被「合并」到同一个容器中。挂接到响应:路径操作函数执行结束后,fastapi/routing.py 的
_build_response_args()会把background_tasks放入响应参数{"background": solved_result.background_tasks};在内部响应处理流程里,若响应对象尚未携带 background,也会执行raw_response.background = solved_result.background_tasks这类赋值,将后台任务绑定到最终返回的 StarletteResponse上。最终由 ASGI 服务器在响应体发送完毕后逐个执行这些任务。
这里其实已经体现了一个关键结论:BackgroundTasks是绑定在单次请求/响应生命周期上的,任务执行发生在当前进程、当前请求结束之后,且需要响应对象被实际发送,因此它适合同进程内的轻量任务,而非跨进程的分布式任务。
技术细节:BackgroundTasks与BackgroundTask别搞混
- FastAPI 中的
BackgroundTasks(带 s,复数)类直接继承自 Starlette 的starlette.background.BackgroundTasks,见 fastapi/background.py。FastAPI 将其直接导入并暴露为fastapi.BackgroundTasks,目的就是让你能安全地从fastapi导入,避免误从starlette.background导入不带 s 的BackgroundTask(单数)。 BackgroundTasks(复数)是任务的集合容器,用来管理一组任务;BackgroundTask(单数)代表单个后台任务。
当且仅当使用复数形式的BackgroundTasks作为路径操作函数参数时,FastAPI 才能替你完成「创建对象→注入→合并→绑定响应」这一整套流程。FastAPI 内部在识别参数时按类型进行判定,两者在类型系统上属于不同类,混用时行为并不相同。
如果你确实要单独使用BackgroundTask(单数),也是可行的,但需要自己负责:在代码中手动创建该对象,并返回一个包含它的 StarletteResponse(例如构造Response(..., background=BackgroundTask(func, ...)))。
适用边界:何时用BackgroundTasks,何时换 Celery
需要明确的是,BackgroundTasks不是通用的任务队列系统,官方教程也给出了清晰的取舍建议:
- 如果需要执行重量级后台计算,且不要求它一定运行在同一个进程内(例如不需要共享内存、共享变量),那么更合适的是 Celery 一类的专业任务队列工具;
- 这类工具通常需要更复杂的配置,以及 RabbitMQ、Redis 之类的消息/任务队列中间件,代价是可以在多个进程乃至多台服务器上分布式运行任务;
- 但反过来,如果你的任务需要访问同一个 FastAPI 应用内的变量与对象,或者只是少量轻量任务(如发送一封邮件通知),直接用
BackgroundTasks是最简、最省事的方案——零额外依赖、零中间件、几行代码即可完成。
小结
使用BackgroundTasks的核心套路可以概括为:
- 从
fastapi导入BackgroundTasks; - 在路径操作函数或依赖(含子依赖)中声明
BackgroundTasks类型参数; - 用
.add_task(task_func, *args, **kwargs)注册要延迟执行的函数; - FastAPI 自动注入、跨层合并同一对象上的全部任务,并在响应发送后依次执行。
它非常适合邮件通知、日志写入、轻量数据处理等「响应当即返回、任务稍后完成」的场景;需要跨进程、跨服务器的重量级任务时,再考虑引入 Celery 等任务队列系统。
【免费下载链接】fastapiFastAPI framework, high performance, easy to learn, fast to code, ready for production项目地址: https://gitcode.com/GitHub_Trending/fa/fastapi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考