Skip to content

异步与并发

async def vs def、阻塞调用处理、并发场景。

Updated View as Markdown
For humans

异步与并发

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]     # 并发请求, 总耗时≈最慢单个
    )

面试追问

  1. async def 和 def 路由的区别? async 在事件循环执行,def 在线程池执行(默认约 40 线程)。FastAPI 自动分流
  2. async 路由里能调同步库吗? 能但会阻塞整个事件循环(所有请求停摆)。用 to_thread 或改成 def 路由
  3. 什么时候用 async? 用异步库(asyncpg、httpx async)做 IO 时。同步 ORM/requests 用 def 更安全
  4. 并发请求多个 API 怎么写? asyncio.gather + AsyncClient,总耗时约等于最慢单个请求
  5. CPU 密集任务怎么办? 线程池受 GIL 限制无效,用多进程或 Celery 队列
Navigation

Type to search…

↑↓ navigate↵ selectEsc close