
文章目录
- 1. 课前导读
- 2. 核心理论讲解
- 3. 环境搭建 & 实操准备
- 4. 手把手代码实战
- 5. 重点知识点总结
- 6. 课后作业 & 思考题
- 7. 本节干货总结
- 下节课预告
- 🔗《20节课 FastAPI 从入门到精通》系列课程导航
1. 课前导读
本节课学习目标
在前15课中,我们已经能够开发功能完善的RESTful API,并集成了MySQL数据库实现了CRUD操作。你可能已经注意到,FastAPI的路由函数既可以定义为def(同步)也可以定义为async def(异步)。那么,这两者有什么区别?为什么FastAPI能够比传统的Flask框架处理更高的并发?答案就在于异步编程。
本节课将从理论到实践,系统讲解Python异步编程的核心概念,以及如何在FastAPI中充分利用异步特性构建高性能接口。学完本节课,你将:
- 理解异步编程原理:事件循环、协程、
async/await的底层逻辑 - 掌握asyncio基础:创建任务、并发执行、同步原语
- 优化异步数据库操作:确保数据库驱动为异步,避免阻塞事件循环
- 实现并发请求聚合:使用
asyncio.gather同时调用多个外部API或数据库查询,大幅降低接口响应时间 - 区分同步与异步路由:知道何时使用
async def、何时使用普通def,避免误区 - 解决异步中的常见陷阱:混用同步阻塞库、忘记
await、事件循环阻塞等
前置知识
- 已完成第15课,掌握SQLAlchemy异步ORM的基本使用
- 了解Python函数定义,对
async/await有基础认知(即使不熟悉也没关系,本课会详细讲解) - 了解HTTP协议和并发的基本概念
学完能掌握什么
学完本节课后,你将具备以下能力:
- 编写高效的异步接口:在IO密集型场景下,让API并发能力提升数倍
- 使用
asyncio.gather并发执行多个数据库查询:将串行300ms的总时间降低到并行100ms - 集成异步HTTP客户端
httpx:并发调用多个第三方API,聚合结果 - 正确使用
BackgroundTasks和asyncio.create_task:处理后台任务,不阻塞响应 - 识别和解决异步代码中的阻塞点:将同步库放到线程池中执行,保持事件循环流畅
- 为生产环境性能调优打下基础:理解异步与多进程的区别,合理部署
适用人群
- 对Python异步一知半解,想要彻底搞懂的开发者
- 已经用FastAPI写了接口,但不确定自己的代码是否真正异步的工程师
- 接口性能不佳,希望通过并发优化响应时间的后端开发
- 准备构建高并发、高吞吐量API的架构师
2. 核心理论讲解
2.1 同步 vs 异步:从餐厅服务讲起
同步模型(如Flask、Django传统模式):
服务员(线程)接到订单后,必须等待厨师做完菜(IO操作),期间不能服务其他客人。为了服务多位客人,餐厅必须雇佣很多服务员(多线程/多进程),但每个服务员占用大量资源。
异步模型(如FastAPI + 异步驱动):
服务员(协程)接到订单后,只需将订单交给厨师,然后立即去服务下一位客人;当厨师做好菜时,服务员再回来取菜送给客人。一个服务员可以同时处理成百上千个订单,资源占用极少。
核心区别:
- 同步:IO操作时线程阻塞,等待完成。
- 异步:IO操作时协程主动让出控制权,事件循环调度其他协程执行,IO完成后恢复。
2.2 事件循环与协程的底层逻辑
事件循环(Event Loop) 是异步编程的引擎。它不断循环检查任务队列,执行可运行的协程,处理IO事件。可以理解为“总调度室”。
协程(Coroutine) 是一种特殊的函数,可以在执行过程中暂停(await),让出控制权给事件循环,等待某个IO操作完成后恢复执行。
async def 和 await 的作用:
async def:定义一个协程函数,调用时返回协程对象,不会立即执行。await:暂停当前协程,等待另一个协程或Future完成,期间事件循环可以执行其他任务。
简单示例:
import asyncio
async def say_after(delay, msg):
await asyncio.sleep(delay) # 模拟IO操作
print(msg)
async def main():
# 并发执行两个任务
task1 = asyncio.create_task(say_after(1, "Hello"))
task2 = asyncio.create_task(say_after(2, "World"))
await task1
await task2
asyncio.run(main())
2.3 FastAPI如何处理同步与异步路由
FastAPI支持两种定义方式:
@app.get("/sync")
def sync_route():
return {"message": "同步路由"}
@app.get("/async")
async def async_route():
return {"message": "异步路由"}
工作原理:
- 如果路由是
async def,FastAPI会直接将其作为协程在事件循环中调用。 - 如果路由是
def,FastAPI会使用run_in_executor将其放到线程池中执行,避免阻塞事件循环。
性能影响:
- IO密集型操作(数据库查询、HTTP请求、文件读写):应使用
async def+异步库,真正实现高并发。 - CPU密集型操作(计算、加密、图像处理):应使用
def,交由线程池处理,避免占用事件循环。或者使用asyncio.to_thread。
重要:如果在async def路由中调用了同步阻塞库(如requests.get()),整个事件循环会被阻塞,所有请求串行执行,性能急剧下降。必须使用异步版本(如httpx.AsyncClient)。
2.4 异步数据库驱动的必要性
在第14课中,我们使用了aiomysql作为MySQL的异步驱动,配合SQLAlchemy异步引擎。为什么不能用pymysql?
pymysql是同步驱动,执行查询时会阻塞当前线程。如果在async def路由中使用,会阻塞事件循环。aiomysql底层使用asyncio进行非阻塞IO,查询期间事件循环可以处理其他请求。
验证阻塞:编写一个使用同步驱动的接口,然后使用time.sleep模拟,你会发现并发请求全部串行处理。而异步驱动下,await asyncio.sleep不会阻塞。
2.5 并发执行:asyncio.gather vs 串行
假设我们需要在同一个接口中查询用户信息、订单列表、商品推荐三个独立数据源。如果串行执行:
用户查询: 50ms → 订单查询: 80ms → 推荐查询: 100ms → 总耗时 230ms
如果并发执行(同时发起三个查询),总耗时仅为最慢的那个(100ms),性能提升130%。
使用asyncio.gather:
results = await asyncio.gather(
fetch_user(),
fetch_orders(),
fetch_recommendations()
)
2.6 与Flask/Django的异步对比
| 框架 | 原生异步支持 | 异步ORM/驱动 | 并发能力 | 学习曲线 |
|---|---|---|---|---|
| FastAPI | 完全异步(ASGI) | SQLAlchemy 2.0异步、asyncpg、aiomysql | 极高 | 中等 |
| Flask | 同步(WSGI),可通过Quart变异步 | 需额外配置 | 低(多线程) | 低 |
| Django 3.1+ | 部分异步视图,但ORM同步 | 需使用asyncio封装或第三方 | 有限 | 中高 |
FastAPI基于Starlette(ASGI),天然支持WebSocket、HTTP/2,异步生态成熟。
3. 环境搭建 & 实操准备
3.1 复用项目环境
继续使用第15课的项目(已完成数据库集成)。确保虚拟环境已激活。
cd ~/fastapi_course/lesson03_first_project
source venv/bin/activate
3.2 安装异步HTTP客户端
为了演示并发调用外部API,安装httpx(支持异步):
pip install httpx
3.3 确认数据库驱动为异步
检查.env中的DATABASE_URL是否使用aiomysql:
DATABASE_URL=mysql+aiomysql://root:password@localhost:3306/fastapi_course?charset=utf8mb4
3.4 新增代码文件
创建app/api/v1/async_demo.py,并在main.py中注册。
touch app/api/v1/async_demo.py
main.py中注册:
from app.api.v1 import async_demo
app.include_router(async_demo.router, prefix="/api/v1/async", tags=["异步编程演示"])
为了模拟真实场景,我们也会创建一个简单的第三方API模拟服务(使用FastAPI的另一个实例或使用公共API)。本课使用httpbin.org作为演示。
4. 手把手代码实战
4.1 asyncio基础:协程、任务、事件循环
首先,创建一个演示协程基础的路由,不涉及HTTP,仅帮助理解概念。
# app/api/v1/async_demo.py
import asyncio
from fastapi import APIRouter
router = APIRouter()
@router.get("/basics")
async def async_basics():
"""
演示异步基础概念
"""
async def hello_world():
await asyncio.sleep(0.1)
return "Hello"
async def foo():
await asyncio.sleep(0.2)
return "World"
# 串行执行
start = asyncio.get_event_loop().time()
result1 = await hello_world()
result2 = await foo()
serial_time = asyncio.get_event_loop().time() - start
# 并发执行
start = asyncio.get_event_loop().time()
results = await asyncio.gather(hello_world(), foo())
parallel_time = asyncio.get_event_loop().time() - start
return {
"serial_result": f"{result1} {result2}",
"serial_time": round(serial_time * 1000, 2),
"parallel_result": " ".join(results),
"parallel_time": round(parallel_time * 1000, 2),
"conclusion": f"并发节省了 {round((serial_time - parallel_time) * 1000, 2)}ms"
}
访问/api/v1/async/basics,可以看到并发比串行快约100ms(因为两个sleep并行)。
代码说明:
asyncio.sleep是一个异步等待操作,模拟IO等待。asyncio.gather并发执行多个协程,等待所有完成。- 使用事件循环的时间戳测量耗时。
4.2 同步阻塞 vs 异步非阻塞路由对比
为了直观感受,我们创建两个对比接口:一个使用同步阻塞(time.sleep),一个使用异步非阻塞(asyncio.sleep)。
import time
@router.get("/blocking")
def blocking_route():
"""同步阻塞路由 - 会阻塞整个服务器"""
time.sleep(3) # 模拟阻塞操作
return {"message": "完成"}
@router.get("/non-blocking")
async def non_blocking_route():
"""异步非阻塞路由 - 不会阻塞"""
await asyncio.sleep(3) # 异步等待,让出控制权
return {"message": "完成"}
测试方法:使用浏览器或curl同时发送多个请求到/blocking,你会注意到第二个请求需要等待第一个完成后才能开始(串行)。而/non-blocking多个请求可以同时处理。
注意:在生产环境中,绝不要在async def路由中使用time.sleep,必须用asyncio.sleep。
4.3 异步数据库查询(使用现有User模型)
我们使用第15课的用户表,编写一个接口并发查询多个用户。
from sqlalchemy import select
from app.database import get_db
from app.models.user import User
from app.schemas.user import UserResponse
@router.get("/users/concurrent")
async def get_users_concurrent(
db: AsyncSession = Depends(get_db),
ids: str = Query("1,2,3", description="用户ID列表,逗号分隔")
):
"""
并发查询多个用户,使用 asyncio.gather
"""
id_list = [int(i.strip()) for i in ids.split(",")]
async def fetch_one(user_id: int):
stmt = select(User).where(User.id == user_id)
result = await db.execute(stmt)
user = result.scalar_one_or_none()
return user
# 并发执行所有查询
users = await asyncio.gather(*[fetch_one(uid) for uid in id_list])
# 过滤掉不存在的用户
valid_users = [UserResponse.model_validate(u) for u in users if u]
return {"total_requested": len(id_list), "found": len(valid_users), "users": valid_users}
性能分析:如果串行查询3个用户,每个耗时50ms,总耗时150ms;并发查询总耗时仍为50ms(假设数据库连接池足够)。
注意:这里每个查询都使用了同一个db会话,SQLAlchemy异步会话是线程安全的,但需要注意的是,asyncio.gather中的协程都共享同一个会话,这通常没问题,因为会话本身支持并发查询(不同的statement)。但如果涉及到事务隔离,需要更谨慎。
4.4 并发调用多个外部API(使用httpx)
真实业务中,经常需要聚合多个第三方服务的数据。使用httpx.AsyncClient并发请求。
import httpx
@router.get("/aggregate")
async def aggregate_apis():
"""
并发调用多个外部API,聚合结果
"""
async def fetch_ip():
async with httpx.AsyncClient() as client:
resp = await client.get("https://httpbin.org/ip")
return resp.json()
async def fetch_user_agent():
async with httpx.AsyncClient() as client:
resp = await client.get("https://httpbin.org/user-agent")
return resp.json()
async def fetch_headers():
async with httpx.AsyncClient() as client:
resp = await client.get("https://httpbin.org/headers")
return resp.json()
# 并发执行
results = await asyncio.gather(fetch_ip(), fetch_user_agent(), fetch_headers())
return {
"ip": results[0],
"user_agent": results[1],
"headers": results[2]
}
测试:访问该接口,总耗时大约等于最慢的那个API(通常几百毫秒),而不是三者之和。
最佳实践:复用httpx.AsyncClient实例,避免每个请求都创建新客户端。可以在应用启动时创建一个全局客户端,使用依赖注入传递。
4.5 异步接口中的CPU密集型任务处理
如果路由中有计算密集型任务(如加密、图片处理),不应在async def中直接执行,否则会阻塞事件循环。解决方案是使用asyncio.to_thread将其放到线程池中执行。
import hashlib
def cpu_intensive_task(data: str, rounds: int = 100000):
"""模拟CPU密集型计算"""
for _ in range(rounds):
hashlib.sha256(data.encode()).hexdigest()
return f"Processed {data}"
@router.get("/cpu-bound")
async def cpu_bound_route(data: str = "test"):
"""
使用 asyncio.to_thread 将CPU密集任务放到线程池执行,避免阻塞事件循环
"""
# 在单独线程中执行同步函数
result = await asyncio.to_thread(cpu_intensive_task, data, 50000)
return {"result": result}
注意:asyncio.to_thread是Python 3.9+引入,对于低版本可以使用loop.run_in_executor。
4.6 后台任务:BackgroundTasks vs asyncio.create_task
FastAPI提供了BackgroundTasks,用于在响应返回后执行轻量级任务。它使用线程池(同步)执行,适合短小任务。对于需要异步支持的任务,可以使用asyncio.create_task。
from fastapi import BackgroundTasks
async def async_background_task(message: str):
await asyncio.sleep(2)
print(f"后台任务完成: {message}")
@router.post("/bg-task")
async def bg_task_endpoint(background_tasks: BackgroundTasks, msg: str = "hello"):
# FastAPI的BackgroundTasks会在线程池中执行同步函数,不能直接接受async函数
# 如果需要异步后台任务,需手动创建task
asyncio.create_task(async_background_task(msg))
return {"message": "请求已接收,后台任务执行中"}
警告:asyncio.create_task创建的任务如果未等待,在FastAPI应用中可能随主事件循环关闭而消失。对于可靠的后台任务,应使用Celery等任务队列。
4.7 异步上下文管理器与资源管理
异步上下文管理器(async with)用于管理需要异步初始化和清理的资源,如数据库连接、HTTP客户端。
class AsyncResource:
async def __aenter__(self):
print("获取资源")
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
print("释放资源")
await asyncio.sleep(0.1)
@router.get("/context")
async def async_context_demo():
async with AsyncResource() as res:
# 使用资源
await asyncio.sleep(0.5)
return {"message": "ok"}
在FastAPI中,依赖注入的get_db使用了async with来管理会话生命周期,这就是异步上下文管理器的典型应用。
4.8 使用anyio和asyncio的兼容层
FastAPI基于anyio,一个比asyncio更高级的异步库,支持多种后端。但通常使用asyncio即可。
4.9 实战:构建高性能仪表盘接口
假设我们需要实现一个仪表盘,同时获取用户统计、订单统计、商品统计。这三个统计来自不同的数据库查询。使用asyncio.gather并发执行。
@router.get("/dashboard")
async def dashboard(db: AsyncSession = Depends(get_db)):
async def user_count():
result = await db.execute(select(func.count()).select_from(User))
return result.scalar_one()
# 假设存在订单模型和商品模型
# async def order_count(): ...
# async def product_count(): ...
# 并发执行
counts = await asyncio.gather(user_count()) # 实际应包含多个
return {
"user_count": counts[0],
# "order_count": counts[1],
# "product_count": counts[2]
}
由于我们没有定义订单和商品模型,此处仅演示思路。
4.10 避免常见异步陷阱
陷阱1:在async def中调用同步阻塞库
@router.get("/bad")
async def bad_route():
import requests
resp = requests.get("https://httpbin.org/delay/2") # 阻塞事件循环!
return resp.json()
正确做法:使用httpx.AsyncClient。
陷阱2:忘记await
@router.get("/missing-await")
async def missing_await():
result = asyncio.sleep(1) # 返回协程对象,但未await
return {"result": result} # 协程对象无法序列化
正确做法:await asyncio.sleep(1)。
陷阱3:在协程中混合使用time.sleep
@router.get("/bad-sleep")
async def bad_sleep():
time.sleep(2) # 阻塞整个事件循环
return {"ok": True}
正确做法:await asyncio.sleep(2)。
陷阱4:在异步环境中使用asyncio.run()
在已有的事件循环中再次调用asyncio.run()会抛出异常。使用await或asyncio.create_task。
陷阱5:数据库查询未使用异步驱动
即使路由是async def,如果使用的数据库驱动是同步的(如psycopg2、pymysql),查询操作依然会阻塞。务必使用异步驱动。
4.11 性能测试:压测对比
我们可以编写一个简单的性能测试脚本(使用asyncio或locust)来对比同步阻塞接口和异步非阻塞接口的吞吐量。这里给出思路:
# test_perf.py
import asyncio
import httpx
async def send_request(url):
async with httpx.AsyncClient() as client:
resp = await client.get(url)
return resp.status_code
async def main():
urls = ["http://localhost:8000/api/v1/async/non-blocking"] * 50
tasks = [send_request(url) for url in urls]
start = asyncio.get_event_loop().time()
await asyncio.gather(*tasks)
end = asyncio.get_event_loop().time()
print(f"50个请求完成耗时: {end-start:.2f}秒")
asyncio.run(main())
对比/blocking接口,可以看到显著差异。
5. 重点知识点总结
核心语法速查表
| 场景 | 代码 |
|---|---|
| 定义协程 | async def my_coro(): ... |
| 等待协程 | await my_coro() |
| 并发执行多个任务 | results = await asyncio.gather(coro1(), coro2()) |
| 创建任务(不等待) | task = asyncio.create_task(coro()) |
| 同步函数放到线程池 | await asyncio.to_thread(sync_func, arg) |
| 异步sleep | await asyncio.sleep(seconds) |
| 异步HTTP请求 | async with httpx.AsyncClient() as client: resp = await client.get(url) |
| 异步数据库查询 | result = await db.execute(select(...)) |
| 后台任务(异步) | asyncio.create_task(background_coro()) |
异步 vs 同步路由选择指南
| 路由内操作 | 推荐方式 | 原因 |
|---|---|---|
| 仅调用异步库(数据库、HTTP) | async def | 非阻塞,高并发 |
| CPU密集型计算 | def 或 async def + to_thread | 避免阻塞事件循环 |
| 混合:少量异步+少量同步 | async def + 同步部分放到to_thread | 保持事件循环流畅 |
| 几乎全是同步库,无IO等待 | def | 简单,FastAPI会自动线程池 |
易错点与避坑指南
-
❌ 在
async def路由中使用requests库
解决方案:使用httpx.AsyncClient。 -
❌ 忘记
await导致协程未执行
检查所有异步调用前是否有await。 -
❌ 在协程中使用
time.sleep
必须使用await asyncio.sleep()。 -
❌ 混用同步数据库驱动
确保SQLAlchemy引擎使用异步驱动(aiomysql、asyncpg)。 -
❌ 使用
asyncio.run()在已有事件循环中
使用await或asyncio.create_task。 -
❌ 认为
async def会自动使同步代码异步
只有使用了异步IO库才能真正非阻塞。
最佳实践
- IO密集型接口统一使用
async def,并确保所有依赖库都是异步版本。 - 对于CPU密集型任务,使用
asyncio.to_thread或单独部署为微服务。 - 复用HTTP客户端实例:在应用启动时创建
httpx.AsyncClient,通过依赖注入共享。 - 使用
asyncio.gather而不是串行await,提升并发效率。 - 设置合理的超时:对异步操作使用
asyncio.wait_for(coro, timeout),避免无限等待。 - 在生产环境中监控事件循环延迟:如果事件循环被阻塞,会导致所有请求延迟增加。
6. 课后作业 & 思考题
实操练习题(必做)
-
基础练习:改造第15课的用户列表接口,使其支持异步并发统计
- 在原有分页基础上,增加一个统计字段
total_orders(假设存在订单表),使用asyncio.gather并发查询用户列表和总订单数。
- 在原有分页基础上,增加一个统计字段
-
进阶练习:实现一个接口,并发调用三个公共API(如天气、新闻、汇率),聚合返回
- 使用
httpx.AsyncClient - 处理其中一个API超时的情况(使用
asyncio.wait)
- 使用
-
挑战练习:实现一个异步WebSocket广播服务
- 创建WebSocket端点,客户端连接后,服务端每秒推送当前时间
- 使用
asyncio.create_task管理每个连接的发送任务
理论思考题
- 为什么在
async def路由中调用同步阻塞库会严重降低并发能力? 请从事件循环角度解释。 asyncio.gather和asyncio.wait的区别是什么? 分别适用于什么场景?- FastAPI的
BackgroundTasks与asyncio.create_task有什么不同? 何时使用哪个? - 如何检测异步代码中的阻塞点? 有哪些工具或方法?
拓展学习方向
- 阅读
asyncio官方文档:深入了解事件循环策略、传输协议等高级特性 - 学习
anyio和trio:更安全的异步库,FastAPI底层使用anyio - 使用
asyncio.run_in_executor与concurrent.futures.ProcessPoolExecutor:处理多核CPU密集型任务 - 异步设计模式:生产者-消费者模式、工作队列等
7. 本节干货总结
核心考点(面试/自测)
-
什么是协程?
async/await的作用是什么?
协程是可以暂停执行的函数,async定义协程,await暂停等待其他协程完成。 -
FastAPI中同步路由和异步路由的性能区别?
异步路由配合异步IO可以实现高并发,同步路由会占用线程资源,并发能力较低。 -
如何并发执行多个数据库查询?
使用asyncio.gather同时发起多个查询,等待所有结果返回。 -
在
async def路由中执行CPU密集型任务会有什么问题?
阻塞事件循环,导致所有请求排队。应使用asyncio.to_thread。 -
为什么不能用
requests库而要用httpx.AsyncClient?
requests是同步库,会阻塞事件循环;httpx.AsyncClient是异步非阻塞的。
实际工作应用场景
-
场景1:数据聚合服务
前端需要同时获取用户信息、订单列表、消息未读数。后端使用asyncio.gather并发查询三个数据源,响应时间从串行300ms降至100ms。 -
场景2:第三方API调用
在用户登录后,并发调用多个外部服务(获取头像、积分、会员等级)来构建用户中心。 -
场景3:WebSocket实时推送
使用asyncio.create_task为每个连接维护发送心跳的协程。 -
场景4:异步文件处理
接收上传文件后,将处理任务放入后台异步执行(如转码、压缩),立即返回“已接收”。 -
场景5:高并发秒杀接口
利用异步非阻塞的特性,结合Redis等异步客户端,处理大量并发请求。
下节课预告
本节课我们深入掌握了异步编程,并能够编写高性能的异步数据库和HTTP调用。现在,我们的API可以高效处理请求了,但还缺少一个关键组件——用户认证与授权。在真实系统中,我们需要区分不同用户,限制其访问资源。
下节课(第17课),我们将学习:
- JWT令牌生成与验证:使用
python-jose库 - 登录接口实现:验证用户名密码,返回access token
- 依赖注入保护接口:使用
Depends校验token,获取当前用户 - 路由级别权限控制:区分普通用户和管理员角色
让你的API具备完善的安全防护能力!下节课见!
🔗《20节课 FastAPI 从入门到精通》系列课程导航
🌟 感谢您耐心阅读到这里!
💡 如果本文对您有所启发欢迎:
👍 点赞📌 收藏 📤 分享给更多需要的伙伴。
🗣️ 期待在评论区看到您的想法, 共同进步。
🔔 关注我,持续获取更多干货内容~
🤗 我们下篇文章见~

773

被折叠的 条评论
为什么被折叠?



