Python 异步生成器是什么?async generator 和普通 generator 有什么区别?
简化版
异步生成器是在 async def 函数里使用 yield 的对象,适合异步地产生一串结果,例如分页拉取、流式读取、SSE 输出。普通生成器用 for 和 next(),异步生成器用 async for 和 anext(),每次取值过程可以 await I/O。
详细版
普通生成器解决“按需产生值”,但每次产生值的过程是同步的。异步生成器允许在产出下一个值之前等待异步 I/O,比如从网络读取一块数据、从数据库游标拉一批记录。
async def fetch_pages():
page = 1
while True:
data = await fetch_page(page)
if not data:
break
yield data
page += 1
async for page in fetch_pages():
...
面试要强调:异步生成器不是并发执行多个 yield,而是每次迭代都由事件循环驱动,能在等待 I/O 时让出控制权。它非常适合流式处理和大数据量分批处理。
完整版教学
一、为什么需要异步生成器
有些数据既不能一次性全部加载,又需要异步等待来源。比如 HTTP 流式响应、数据库异步游标、消息队列消费、分页 API 拉取。普通列表会占内存,普通生成器又不能在取下一项时 await。
一次性列表: 内存压力大
普通生成器: 不能 await 异步 I/O
异步生成器: 边等 I/O,边产出
假设每页 1000 条数据,每条 1KB,总共 100 页。一次性加载约 100MB;异步生成器可以每次只处理 1MB 左右,并在等待下一页网络响应时让出事件循环。
二、语法和协议有什么不同
普通生成器用 def + yield;异步生成器用 async def + yield。消费时普通生成器用 for,异步生成器用 async for。
def gen():
yield 1
async def agen():
yield 1
| 类型 | 定义 | 消费 | 下一项 |
|---|---|---|---|
| 普通生成器 | def + yield | for | next() |
| 异步生成器 | async def + yield | async for | await anext() |
异步生成器实现的是异步迭代协议:__aiter__ 和 __anext__。每次取下一项都可能 await,因此必须在异步上下文中消费。
三、异步生成器的执行流程
异步生成器函数被调用时,不会立即执行函数体,而是返回异步生成器对象。async for 开始迭代时,事件循环驱动它执行到下一个 yield,产出值后暂停;下一轮继续从暂停点恢复。
调用 agen() -> 得到异步生成器对象
async for 第一次 -> 执行到 yield 1 -> 暂停
async for 第二次 -> 从暂停点继续 -> await I/O -> yield 2
这和普通生成器的暂停/恢复类似,只是恢复过程本身可以包含 await,不会阻塞整个线程。
四、流式响应中的应用
Web 服务里常用异步生成器做流式响应。比如逐块读取文件、逐条输出模型生成结果、逐行推送日志。每次 yield 一小块,客户端就能更早收到数据。
async def stream_tokens():
async for token in llm_stream():
yield token.encode("utf-8")
如果 1000 个 token 每个 20ms 产生,一次性等全部生成要 20 秒后才返回;流式输出可以在第一个 token 生成后就发送,用户体验差很多。
异步生成器的价值不是让一个生成器内部并行,而是让“等下一块数据”的时间不阻塞事件循环。
五、资源清理要注意什么
异步生成器可能持有连接、文件、游标等资源。如果消费方提前 break 或取消任务,生成器要能正确清理。可以使用 try/finally,也可以结合异步上下文管理器。
async def rows(conn):
cursor = await conn.cursor()
try:
async for row in cursor:
yield row
finally:
await cursor.close()
如果忽略清理,长连接服务里可能逐渐泄露游标和连接。异步流式代码尤其要考虑取消路径,因为客户端断开是非常常见的。
六、和队列、Task 的关系
异步生成器适合一个生产流程按需产出;asyncio.Queue 更适合多个生产者和消费者解耦。不要把所有流式场景都塞进生成器,也不要为简单分页消费引入复杂队列。
| 场景 | 更适合 |
|---|---|
| 单一路径分页读取 | 异步生成器 |
| HTTP 流式响应 | 异步生成器 |
| 多生产者多消费者 | asyncio.Queue |
| 后台任务独立运行 | Task / 任务队列 |
选择标准是是否需要缓冲、并发生产和消费、背压控制。如果只是“下一项来自一次 await”,异步生成器往往最清晰。
七、常见误区与追问
- 误区:异步生成器会自动并发生成所有值。 不会,它仍是按需迭代,只是在等待 I/O 时让出控制权。
- 误区:可以用普通 for 遍历异步生成器。 不行,要用
async for或await anext()。 - 误区:yield 后资源会自动清理。 提前取消和 break 时要设计 finally 清理。
- 追问:异步生成器和协程有什么区别? 协程通常最终返回一个值;异步生成器可以多次 yield 值。
- 追问:异步生成器能 return 值吗? 可以结束,但不像普通函数那样给
async for返回业务值。 - 追问:什么时候用 Queue 而不是异步生成器? 多生产者/多消费者、需要缓冲和背压时用 Queue。
- 追问:流式响应为什么适合异步生成器? 它天然按块产出,每块之间可以 await I/O。
八、加强记忆
异步生成器记成“能 await 的 yield”。它保留生成器按需产出的内存优势,又允许每次取下一项时等待网络、数据库或流式模型输出。使用时抓三点:async for 消费、取消路径清理、复杂并发改用 Queue 或任务管理。