异步与并发
FastAPI 同时支持同步和异步路由,理解两者的执行模型是写对并发代码的前提。面试主线:async def 和 def 区别、阻塞调用怎么处理。
async def vs def
@app.get("/async") # 异步路由
async def async_route():
await asyncio.sleep(1) # 不阻塞其他请求
return {"ok": True}
@app.get("/sync") # 同步路由
def sync_route():
time.sleep(1) # 阻塞线程池里的一个线程
return {"ok": True}| 维度 | async def | def |
|---|---|---|
| 执行位置 | 事件循环(单线程) | 线程池(默认约 40 线程,新版本按 CPU 核数调整) |
| 并发能力 | IO 等待时切换,单线程服务大量请求 | 靠线程池,线程有限 |
| 阻塞影响 | 阻塞整个事件循环(所有请求卡住) | 只占一个线程 |
| 适用 | 异步库 IO(httpx async、asyncpg) | 同步库(requests、psycopg) |
关键结论:async def 里绝不能有同步阻塞调用;用同步库(ORM、requests)时用 def 路由,FastAPI 自动丢线程池。
阻塞调用处理
@app.get("/heavy")
async def heavy():
result = await asyncio.to_thread(sync_compute, data) # 丢线程池
return result
# 或使用 run_in_executor 自定义线程池
loop = asyncio.get_running_loop()
result = await loop.run_in_executor(None, sync_compute, data)原则:
- 异步代码里碰到同步阻塞(CPU 计算、同步 IO、同步库),用
asyncio.to_thread丢线程池 - CPU 密集任务线程池也帮不上(GIL),上多进程或独立任务队列
- 数据库:同步 SQLAlchemy 配 def 路由;asyncpg/异步 SQLAlchemy 配 async def
并发场景实践
| 场景 | 正确姿势 |
|---|---|
| 并发请求多个 API | asyncio.gather(httpx.AsyncClient) |
| 大列表逐项处理 | asyncio.Semaphore 限并发,防打爆下游 |
| 定时任务 | 后台 Task 或 APScheduler,别放请求路径 |
| 同步第三方库 | def 路由或 to_thread |
| CPU 密集 | 多进程(ProcessPoolExecutor)或 Celery |
async with httpx.AsyncClient() as client:
results = await asyncio.gather(
*[client.get(u) for u in urls] # 并发请求, 总耗时≈最慢单个
)面试追问
- async def 和 def 路由的区别? async 在事件循环执行,def 在线程池执行(默认约 40 线程)。FastAPI 自动分流
- async 路由里能调同步库吗? 能但会阻塞整个事件循环(所有请求停摆)。用 to_thread 或改成 def 路由
- 什么时候用 async? 用异步库(asyncpg、httpx async)做 IO 时。同步 ORM/requests 用 def 更安全
- 并发请求多个 API 怎么写? asyncio.gather + AsyncClient,总耗时约等于最慢单个请求
- CPU 密集任务怎么办? 线程池受 GIL 限制无效,用多进程或 Celery 队列