FastAPI异步编程完全指南
📂 所属阶段:第二阶段 — 进阶黑科技(核心篇)
🔗 相关章节:FastAPIdependency-injection · FastAPImiddleware-application
目录
什么是异步编程?
同步 vs 异步:排队打饭的比喻 🍚
想象你去食堂打饭:
- 同步:你站在窗口等厨师炒完一盘菜,端走,再点下一道。厨师在炒菜时你只能干等。
- 异步:你把菜单交给厨师,然后去拿餐具、倒饮料,等菜好了服务员直接端到你桌上。你不需要傻站着等。
这就是核心区别:等待时是否可以切换去干别的事。
为什么 Web 服务需要异步?
假设有三个请求同时到达:
- 同步模式:按顺序处理,总耗时 = 200 + 1 + 1000 = 1201ms
- 异步模式:A 等待数据库时切换 B,B 完成切 C,C 等待时切回 A → 总耗时约 1000ms
在 I/O 密集型场景(数据库查询、HTTP 请求、文件读写),异步能让单线程处理大量并发请求,显著提升 API 性能。
异步编程的三大优势
- 高并发处理能力:单个进程可以处理数千个并发连接
- 资源利用率高:避免线程创建和切换的开销
- 响应速度快:I/O 等待期间可以处理其他请求
async/await 详解
基础语法
await 在等什么?
await 只能等待可等待对象(Awaitable),包括:
asyncio.sleep vs time.sleep
⚠️ 在 FastAPI 中用
time.sleep()会阻塞整个事件循环!
事件循环原理
什么是事件循环?
事件循环是异步的"调度中心",工作流程:
事件循环生命周期演示
异步函数调用规则
四条黄金法则
异步中调用耗时同步函数
为什么不能直接 await 同步函数?
因为同步函数不是可等待对象,它会阻塞整个线程。必须用 asyncio.to_thread() 将其扔到单独的线程池执行,这样事件循环才能继续处理其他任务。
FastAPI 中的异步使用
异步路由 vs 同步路由
FastAPI 会自动检测路由类型:
- 如果定义为
async def,会在事件循环中运行,遇到await就挂起 - 如果定义为
def,会被丢到线程池中执行,不阻塞主循环(但增加少量线程开销)
异步 HTTP 请求(以 httpx 为例)
💡 使用
httpx.AsyncClient作为上下文管理器,可以复用连接,比每次临时创建AsyncClient更高效。
何时使用同步代码
同步代码的适用场景
经验法则:
- 任务以 I/O 为主(网络、磁盘、数据库) ➔ 用
async def - 任务以计算为主(CPU 密集) ➔ 用
def,FastAPI 自动丢线程池 - 若不确定,可以先写成
def,性能测试后再优化。
常见陷阱与避坑指南
陷阱 1:忘记 await
陷阱 2:循环中串行 await
陷阱 3:在异步函数中混用同步阻塞调用
修复方法:
- 换成
await asyncio.sleep(2) - 或把
time.sleep放进await asyncio.to_thread(time.sleep, 2)
陷阱 4:误用全局变量
由于事件循环是单线程,修改变量无需加锁,但异步任务之间的执行顺序不确定,过度依赖共享状态容易出 bug。建议每个请求使用局部状态。
实战:构建异步 API 服务
下面演示一个实际例子:并发获取本地“数据库”数据与外部 GitHub API 数据。
运行效果:
两个独立 I/O 操作同时进行,总耗时取决于最慢的那一个,而不是两个操作耗时之和。
性能优化建议
- 使用连接池
如httpx.AsyncClient、asyncpg.create_pool,避免为每个请求新建连接。 - 适当缓存
同步计算可用functools.lru_cache,异步缓存可选asyncio.LifoQueue或三方库。 - 避免不必要的串行 await
独立的任务统一用asyncio.gather并发执行。 - 监控事件循环延迟
生产环境可接入asyncio调试模式或使用工具如prometheus_client观察阻塞情况。
总结
💡 记住:异步的核心是高效处理大量并发 I/O 请求,但不要在异步函数中做 CPU 密集计算——那会让整个事件循环卡住。合理使用线程池,让同步与异步各司其职,才能最大化 FastAPI 的性能。

