FastAPI 并发与 async/await:路径操作函数的并发模型、线程池执行机制与协程原理详解
本文基于 FastAPI 官方文档《Nebenläufigkeit und async / await》(并发与 async/await 章节)展开,系统讲解在 FastAPI 路径操作函数(path operation functions)中何时该用 async def、何时该用普通 def 的完整决策准则;并深入 FastAPI 源码,展示框架如何用 run_in_threadpool 将同步函数卸载到外部线程池、如何区分协程函数、以及依赖(Dependencies)与带 yield 的生成器依赖在线程池中的执行细节,帮助读者既掌握日常写法,又理解底层并发原理。
一、快速决策指南(TL;DR)
如果你使用的第三方库必须用 await 调用,例如:
results = await some_library()
那么请将你的路径操作函数声明为 async def:
@app.get('/')
async def read_results():
results = await some_library()
return results
注意:
await只能用在用async def声明的函数内部。
如果你使用的第三方库与外部系统(数据库、API、文件系统、网络等)交互,但不支持 await——目前绝大多数数据库库都是如此(同步阻塞库),那么就用普通的 def 声明路径操作函数:
@app.get('/')
def results():
results = some_library()
return results
如果你的应用(无论如何)不需要与其他东西通信并等待其响应,那么即使内部没有 await,也请使用 async def。
如果你不确定,就用普通的 def。
要点:你可以在路径操作函数中自由混用 def 与 async def,每个函数各自采用最合适的声明方式,FastAPI 都会正确处理。无论哪种情况,FastAPI 整体上始终是异步运行的、性能都极高;但遵循上述准则后,还能进一步榨取性能优化空间。
这四条准则构成了全文的“操作骨架”,后文所有技术细节都是为了说明这些准则为何成立。
二、异步代码:程序为什么需要“等待”
现代 Python 支持使用所谓的协程(Coroutines)来编写异步代码,语法即 async 与 await。这句话包含三个关键词,逐一拆解:
2.1 什么是“异步代码”
“异步”意味着语言提供了一种机制,让程序可以在代码的某个点上“声明”:它需要等待别的事情在别处完成。假设等待的对象是一个“慢文件”操作:在等待期间,系统可以去处理其他任务;等空闲时再回来检查那些等待中的任务是否已完成,并继续处理第一个完成的任务。
“等待别的事情”通常指相对“慢”的 I/O 操作(相对于 CPU 和内存的速度),例如等待:
- 客户端数据通过网络接收完毕;
- 客户端接收你程序发出的数据;
- 系统从磁盘读取文件内容并交给你的程序;
- 系统将你程序写入的内容落盘;
- 远程 API 调用完成;
- 数据库操作完成;
- 数据库查询返回结果;
- 等等。
由于执行时间主要消耗在等待 I/O 上,这类操作被称为**“I/O 密集型”(I/O bound)**。之所以叫“异步”,是因为程序不需要与慢任务“同步”——不必傻等到那一精确时刻任务完成才能接手结果。在异步系统里,任务完成后可以短暂(几微秒)排队,等程序忙完手头的事再回来接收结果。相对地,“同步”也常被称为“顺序(sequentially)”:程序按顺序执行所有步骤(哪怕中间在等待),才切换到下一个任务。
2.2 并发与并行:汉堡比喻
**并发(concurrency)与并行(parallelism)**都指“不同的事情大致同时发生”,但细节差别很大。FastAPI 文档用一个汉堡故事来区分二者。
并发的汉堡:你和对象去快餐店,排队等候前面的人点单。轮到你时点了两个汉堡。收银员转头吩咐后厨开始制作(他还在处理前面顾客的单子)。你付钱,拿到一个取餐号。然后你和对象找位子坐下,边聊边等,偶尔看一眼柜台上方显示的取餐号。等号到了,你去柜台取餐,回到桌上吃汉堡——全程大部分时间你在做别的事(和对象聊天),等待被“外包”给了厨房和取餐号系统。
把这个场景映射到计算机:你是程序。排队时你“闲置”(但队列处理很快,因为收银员只接单不做饭);点单时你做“生产性工作”;随后交互“暂停”(⏸),因为你必须等汉堡做好。但离开柜台后你可以把注意力切换到对象身上,做另一项“生产性工作”。当显示屏跳到你的号码时,你不必立刻跳起来——你有取餐号,别人抢不走你的汉堡。你等当前对话说完(完成当前任务),再去柜台取餐,这一步(取汉堡)完成,同时产生新任务(吃汉堡)。
并行的汉堡:想象 8 位“收银员兼厨师”的快餐店。每位收银员接单后立刻亲自下厨,做完才接下一单。所有顾客都必须留在柜台前等自己的汉堡做完——因为没有取餐号机制。轮到你时,你点单、付钱,然后站在柜台前一直等,同时还得看住别让任何人误拿了你的汉堡。你的对象全程无法获得你的注意力。最后收银员/厨师端着汉堡回来,你取餐、吃、离开。整个过程大量时间都耗在柜台前干等。
映射到计算机:这是一个双处理器系统(你和对象),两个处理器都在“长时间站在柜台前等待”。餐厅有 8 个处理器(收银员/厨师),而并发版餐厅只有 2 个(一个收银员 + 一个厨师)。但用户体验反而更差——因为等待没有被利用起来。
一个更“现实”的并行例子是银行:过去大多数银行有多个柜员和长队,每个柜员顺序地为每个客户做完所有事情,客户必须长时间排队,你的同伴大概率不想陪你去银行办业务。
结论:在“快餐店 + 同伴”场景下,并发系统明显更合理,因为等待时间占比很高——大多数 Web 应用正是如此:大量用户,但服务器大部分时间在等(🕙)用户那不太好的网络把请求发过来,然后又在等响应被接收回去。这些等待单次以微秒计,累积起来却非常可观。因此用异步代码写 Web API 极其划算。这类异步模型让 NodeJS 走红(尽管 NodeJS 并非并行),也是 Go 语言作为编程语言的强项所在。FastAPI 正提供了同一水平的并发能力;而由于 Python 可以同时拥有并发与并行,官方文档引用第三方基准测试(TechEmpower)称其性能可与 Go 这类更接近 C 的编译型语言相当——这一表述出自原文档,属于对特定测试场景的引用,适用前提以原文档为准(“得益于 Starlette”)。
2.3 并发是否比并行更好?
不!这不是故事的寓意。并发不同于并行,它只在需要大量等待的特定场景下更优,因此通常(而非绝对)比并行更适合 Web 应用开发。
文档用一个简短故事做对照:
你有一个又大又脏的房子要打扫。
——对,这就是整个故事。*
这里没有等待,只有多处大量工作。你可以像汉堡例子那样在客厅和厨房之间来回切换(并发),但因为没有任何等待,切换没有任何收益——不管切不切换,完成时间都一样。但如果你把那 8 位前收银员/厨师/现清洁工带来,每人负责一个区域(加上你自己),就能并行地做完所有工作,速度大幅提升。这个场景里每个清洁工就是一个处理器,各自完成自己那部分。由于大部分执行时间花在“干活”(而非等待)上,且计算机的工作由 CPU 执行,这类问题被称为**“CPU 密集型”(CPU bound)**。
典型的 CPU 密集型工作包括需要复杂数学运算的任务:
- 音频或图像处理;
- 计算机视觉:一张图有数百万像素,每个像素含 3 个颜色值,处理通常要对这些像素做大量并行计算;
- 机器学习:通常是大量“矩阵”与“向量”乘法——想象一张巨大的数字表格,要同时把所有数字相乘;
- 深度学习:机器学习的子领域,原理相同,只是表格数量巨大,且训练/推理常用专用处理器(GPU 等)。
2.4 并发 + 并行:Web + 机器学习
FastAPI 让你利用 Web 开发中广泛受益的并发优势(这正是 NodeJS 的主要卖点);同时也能利用并行与多进程(multiprocessing,多个进程并行执行)来处理机器学习系统中的 CPU 密集型负载。再加上 Python 是数据科学、机器学习尤其是深度学习的主力语言这一事实,使 FastAPI 成为构建数据科学/机器学习 Web API 与应用的合适工具(当然不限于此)。生产环境如何做到这一点,可参见官方文档的 Deployment 章节。
三、async 与 await 语法实战
现代 Python 提供了一种非常直观的异步代码写法:它看起来像普通的“顺序”代码,并在正确的时机替你完成“等待”。
如果一个操作需要“等结果返回”,且该操作支持这套新语法,你可以这样写:
burgers = await get_burgers(2)
关键在于 await:它告诉 Python 必须等待 get_burgers(2) 完成,才能把结果存入 burgers。同时 Python 也知道这段时间可以做别的事(比如接收另一个请求)。
为了让 await 生效,它必须位于一个支持异步的函数内部——用 async def 声明:
async def get_burgers(number: int):
# 这里做点异步操作来“制作”汉堡
return burgers
而不是:
# 这不是异步的
def get_sequential_burgers(number: int):
# 这里做点顺序操作来“制作”汉堡
return burgers
用了 async def,Python 就知道该函数体内要留意 await 表达式,可以随时把该函数的执行“暂停”⏸ 转去干别的事,之后再回来继续。
调用 async def 函数时必须“等待”(await)它。因此下面这样写不会工作:
# 不工作,因为 get_burgers 是用 async def 定义的
burgers = get_burgers(2)
所以,只要你使用的库告诉你“可以 await 它”,使用它的路径操作函数就必须用 async def:
@app.get('/burgers')
async def read_burgers():
burgers = await get_burgers(2)
return burgers
3.1 更多技术细节与“鸡蛋问题”
await 只能出现在 async def 函数中;而 async def 函数又必须被 await 才能执行。于是只有 async def 函数内部才能调用 async def 函数——这导致“鸡蛋问题”:第一个 async 函数由谁来调用?
使用 FastAPI 时无需担心:这个“第一个”函数就是你的路径操作函数,FastAPI 知道该怎么处理它(后文源码部分会展示具体机制)。即使不用 FastAPI,也可以直接写自己的异步应用。
Starlette(以及 FastAPI)基于 [AnyIO](此处不附外链),这意味着它同时兼容 Python 标准库的 asyncio 和 Trio 两个异步后端。你甚至可以直接使用 AnyIO 处理更高级的并发用例;官方还基于 AnyIO 提供了 Asyncer 库,作为一层薄封装改善类型标注与自动补全,并配有教程帮助你理解“自己的”异步代码——尤其在必须把异步代码与常规(阻塞/同步)代码组合时特别有用。
3.2 其他异步代码形式
这种 async/await 用法在 Python 中相对较新,但它大幅降低了异步代码的难度。同样的(或几乎相同的)语法近期也被现代 JavaScript(浏览器与 NodeJS)采纳。在此之前,异步代码的处理复杂得多:早期 Python 中人们使用线程或 Gevent,代码更难理解、调试和追踪;早期 NodeJS/浏览器 JavaScript 使用回调,导致“回调地狱”(callback hell)。
四、协程(Coroutines)
协程不过是 async def 函数返回物的一个时髦称呼。Python 知道它是一个“可启动、终会结束、并且可以在每个 await 处随时暂停⏸”的东西。整套用 async/await 编写异步代码的能力常被统称为“使用协程”,类比 Go 语言的核心特性“Goroutines”。
把开头那句话再看一遍:
现代版本的 Python 使用所谓的协程、借助
async与await语法来支持 “异步代码”。
到这里,这句话应该完全通顺了。而这一切正是 FastAPI(经由 Starlette)获得卓越性能的动力来源。
五、源码级印证:FastAPI 如何执行 async def 与 def
以上准则并非只是建议——FastAPI 的源码中对二者的执行路径做了显式区分。以下均以当前仓库源码为准。
5.1 路径操作函数:协程直接 await,同步函数进线程池
文档的“非常技术细节”指出:若用普通 def(而非 async def)声明路径操作函数,它不会被直接调用(那会阻塞服务器),而是在一个外部线程池中执行,然后被 await。
这一点在 路由实现 中一目了然:
async def run_endpoint_function(
*, dependant: Dependant, values: dict[str, Any], is_coroutine: bool
) -> Any:
assert dependant.call is not None, "dependant.call must be set"
if is_coroutine:
return await dependant.call(**values)
else:
return await run_in_threadpool(dependant.call, **values)
即:协程函数直接 await,普通函数走 run_in_threadpool(来自 Starlette/AnyIO 的线程池调度)。is_coroutine 的判定发生在请求处理器构建阶段,见 get_request_handler:
is_coroutine = _is_coroutine_callable(dependant.call)
判定函数 _is_coroutine_callable 内部使用 inspect.iscoroutinefunction 检查(并对 functools.partial、解包后的对象、类实例的 __call__ 等情形做了处理),且结果经 @lru_cache 缓存——从源码结构看,这是针对每个可调用对象只做一次判定、之后零开销的性能优化。
更底层的入口在 request_response(FastAPI 对 Starlette 同名函数的复制改造版):
f: Callable[[Request], Awaitable[Response]] = (
func
if is_async_callable(func)
else functools.partial(run_in_threadpool, func)
)
这里再次确认:异步可调用对象原样执行,同步可调用对象被包进线程池。
文档还特别提醒:如果你从其他异步框架来,习惯给“纯计算、无阻塞”的路径操作函数用普通 def 以换取约 100 纳秒的微小提速,那么在 FastAPI 中效果恰好相反——因为 def 会引入线程池调度开销,这类纯计算场景反而应该用 async def;只有当函数会执行阻塞 I/O 时才用 def。当然即便如此,文档也指出 FastAPI 很可能(依然)比你的旧框架快(或至少相当)。
5.2 依赖(Dependencies):同样的规则
文档说明:依赖若为普通 def 函数(而非 async def),同样在外部线程池执行。这在 依赖求解器 中可以直接验证:
elif _is_coroutine_callable(use_sub_dependant.call):
solved = await call(**solved_result.values)
else:
solved = await run_in_threadpool(call, **solved_result.values)
子依赖可以相互嵌套,一部分用 async def、一部分用普通 def,依然工作正常——def 的那些从线程池取线程执行,而不是被 await。上面的分支逻辑对整棵依赖树中的每一个依赖都适用。
5.3 带 yield 的同步依赖:线程池中的上下文管理器
仓库中还有一个与文档主题强相关的实现细节:contextmanager_in_threadpool。当依赖是普通 def + yield 的生成器时,依赖求解器 会走这条路:
if _is_async_gen_callable(dependant.call):
cm = asynccontextmanager(dependant.call)(**sub_values)
elif _is_gen_callable(dependant.call):
cm = contextmanager_in_threadpool(contextmanager(dependant.call)(**sub_values))
也就是说,同步的 yield 依赖在进入和退出时都会被丢进线程池(__enter__ 走 run_in_threadpool,__exit__ 走 anyio.to_thread.run_sync)。源码注释还解释了为什么 __exit__ 使用一个容量为 1 的独立 CapacityLimiter 而不受默认线程池容量限制:若 __exit__(阻塞操作)因为等不到空闲线程而被卡住,而线程池本身又被依赖内部(例如数据库连接池)占用,就可能产生竞态条件甚至死锁。这是“同步代码必须隔离在事件循环之外”这一原则在边缘场景下的具体体现。
另外,fastapi/concurrency.py 直接转发了 starlette.concurrency 的 run_in_threadpool、iterate_in_threadpool 与 run_until_first_complete,并导入 anyio.to_thread——印证了文档中“Starlette/FastAPI 基于 AnyIO、兼容 asyncio 与 Trio”的说法。
5.4 普通辅助函数:FastAPI 不插手
文档特别强调:你自己直接调用的其他辅助函数,无论用 def 还是 async def 定义,FastAPI 都不影响你调用它们的方式——这与 FastAPI 替你调用的路径操作函数和依赖不同。若辅助函数是普通 def,它就在你的代码里被直接调用(不进线程池);若是 async def,你需要在代码里自己 await 它。换句话说:“进不进线程池”只针对框架替你调用的那一层。
六、小结:一张决策表
综合 TL;DR 准则与源码机制,可归纳为:
| 场景 | 声明方式 | FastAPI 执行方式(源码依据) |
|---|---|---|
需要 await 异步第三方库 |
async def |
直接 await(事件循环内,run_endpoint_function) |
| 使用阻塞库(多数数据库驱动等) | def |
外部线程池执行后再 await(run_endpoint_function) |
| 完全不等待任何东西 | async def |
直接 await |
| 不确定 | def |
线程池执行 |
| 纯计算的“简单”函数(性能敏感) | async def(FastAPI 下与直觉相反) |
避免线程池调度开销 |
同步 yield 依赖 |
def |
__enter__/__exit__ 均走线程池,__exit__ 用独立容量限制防死锁(contextmanager_in_threadpool) |
| 自己调用的辅助函数 | 任选 | FastAPI 不介入,按需 await |
核心心智模型:FastAPI 的事件循环永不被阻塞——同步代码不是被“忽略”,而是被显式卸载到 AnyIO 线程池中执行;async def 与 def 的选择本质上是决定“这段工作”运行在事件循环协程里还是线程池线程里。遵循文档给出的决策准则,你就能在 I/O 密集型 Web 场景拿到并发红利,在 CPU 密集型负载上再叠加多进程并行——这正是 FastAPI 兼顾 NodeJS 式并发能力与 Python 数据科学生态的原因。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0625
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00

