Python 异步编程学习笔记:生成器、异步生成器、async for、asyncio.wait 与 timeout
0. 为什么大模型对话需要异步?
在大模型对话里,尤其是流式输出时,服务端往往不会一次性返回完整答案,而是不断返回一小段内容。
例如 SSE(Server-Sent Events)场景中,模型可能按 token 或 chunk 持续返回:
1 | data: 你好 |
这种数据流的特点是:
- 数据不是一次性准备好的;
- 每次获取下一段数据都可能需要等待网络;
- 等待期间不能阻塞整个程序;
- 最终适合用异步迭代的方式处理。
所以学习 Python 异步时,可以重点掌握下面几类工具:
- 生成器;
- 异步生成器;
- 异步循环
async for; asyncio.wait();async with asyncio.timeout()。
1. 生成器:同步的数据流
生成器是使用 yield 的函数。
它的特点是:不会一次性返回所有结果,而是每次产出一个值。
1 | def gen(): |
输出:
1 | hello |
生成器适合表示“逐步产生的数据”。
比如:
1 | def fake_tokens(): |
这很像大模型 token 流,但它是同步的,不能在里面直接 await 网络请求。
生成器的核心理解:
1 | yield = 产出一个值,然后暂停,下次继续 |
2. 异步生成器:异步数据流的常用写法
异步生成器是:
1 | async def 函数名(): |
例子:
1 | import asyncio |
它和普通生成器最大的区别是:
普通生成器只能同步产出数据;
1 | def gen(): |
异步生成器可以一边等待异步操作,一边产出数据:
1 | async def async_gen(): |
这非常适合网络流式数据,比如 SSE、WebSocket、大模型 token streaming。
3. 手写异步迭代器 vs 异步生成器
传统异步迭代器需要手写 __aiter__() 和 __anext__()。
1 | import asyncio |
使用:
1 | async for item in AsyncCounter(3): |
这套写法比较麻烦。
大多数情况下,可以用异步生成器替代:
1 | import asyncio |
使用方式一样:
1 | async for item in async_counter(3): |
所以可以这样记:
1 | 简单异步数据流:优先用异步生成器 |
异步生成器可以看作是手写异步迭代器的简洁替代方案,但不是所有场景的绝对上位替代。
如果对象内部有复杂状态、多个控制方法、重置逻辑、资源管理逻辑,手写异步迭代器类仍然更合适。
4. 异步循环:async for
async for 是用来遍历异步迭代器的循环语法。
普通循环:
1 | for item in iterable: |
异步循环:
1 | async for item in async_iterable: |
完整例子:
1 | import asyncio |
async for 的特点是:每次获取下一个值时,都可能需要 await。
它大概等价于:
1 | iterator = async_iterable.__aiter__() |
所以它和普通 for 的区别是:
1 | for -> next(iterator) |
适合 async for 的场景:
- SSE 流式响应;
- WebSocket 消息;
- 异步下载;
- 异步数据库游标;
- 大模型 token streaming;
- 网络请求分块读取。
5. SSE / 大模型流式对话中的异步生成器
大模型流式输出很适合抽象成异步生成器。
伪代码:
1 | async def stream_llm_response(): |
使用:
1 | async def main(): |
这就形成了很自然的数据流:
1 | SSE 网络事件 -> async for 接收 -> yield chunk -> 外部 async for 消费 |
在 FastAPI 里也常见类似结构:
1 | async def generate(): |
也就是说,异步生成器是连接“大模型流式接口”和“上层业务代码”的常见桥梁。
6. asyncio.wait():等待多个任务
asyncio.wait() 用来等待一组任务。
它的基本形式是:
1 | done, pending = await asyncio.wait(tasks) |
返回两个集合:
1 | done # 已完成任务 |
例子:
1 | import asyncio |
asyncio.wait() 适合处理“多个任务的状态”。
常见参数:
1 | done, pending = await asyncio.wait( |
return_when 常见取值:
1 | asyncio.ALL_COMPLETED # 默认,等全部完成 |
需要注意:
asyncio.wait() 返回后,pending 里的任务不会自动取消。
如果不想让它们继续运行,需要手动取消:
1 | for task in pending: |
所以可以这样记:
1 | asyncio.wait() 不是直接拿结果,而是查看一组任务的完成状态。 |
Python 官方文档也把 wait() 归类为“monitor for completion”,而 gather() 更偏向“并发调度并等待结果”。官方高层 API 索引中也说明 wait()、gather()、wait_for()、timeout() 都属于任务与超时相关 API。
7. asyncio.wait() 和 gather() 的区别
gather() 更适合:
1 | 我想并发运行多个任务,并拿到所有结果。 |
1 | results = await asyncio.gather(task1, task2, task3) |
wait() 更适合:
1 | 我想知道哪些任务完成了,哪些还没完成。 |
1 | done, pending = await asyncio.wait(tasks) |
对比:
1 | gather() -> 返回结果列表 |
所以:
1 | 要结果:gather() |
8. async with asyncio.timeout():超时控制
Python 3.11 新增了 asyncio.timeout(),它是一个异步上下文管理器。
写法:
1 | async with asyncio.timeout(3): |
意思是:这段异步代码最多运行 3 秒。
完整例子:
1 | import asyncio |
输出:
1 | timeout |
它适合限制一整段异步逻辑:
1 | async with asyncio.timeout(10): |
这比 asyncio.wait_for() 更适合包住多个 await 操作。
9. asyncio.timeout() 和 wait_for() 的区别
asyncio.wait_for() 是给一个 awaitable 加超时:
1 | await asyncio.wait_for(fetch(), timeout=3) |
asyncio.timeout() 是给一段异步代码块加超时:
1 | async with asyncio.timeout(3): |
所以可以这样记:
1 | wait_for() -> 管一个 awaitable |
在大模型流式场景里,asyncio.timeout() 经常更舒服,因为流式过程往往不是一个单独的 await,而是多个步骤加一个 async for。
例如:
1 | async def consume_stream(stream): |
10. 大模型 SSE 场景中的整体结构
可以把一次大模型流式对话理解成:
1 | 1. 发起 HTTP 请求 |
伪代码:
1 | import asyncio |
如果同时请求多个模型:
1 | tasks = [ |
比如“谁先返回就用谁的结果”,这时 asyncio.wait() 就很合适。
11. 最终记忆版
可以按这条线记:
1 | 生成器: |
对于大模型流式对话,最重要的是:
1 | SSE / token streaming 本质上就是一个异步数据流。 |