这篇笔记整理了 Python asyncio 中事件循环、async 与 await、asyncio.create_task、asyncio.gather、AsyncIterator 以及 asyncio.Queue 的用法,最后用一个生产者消费者流水线示例把它们串起来。
一、事件循环与非阻塞 I/O
在同步代码中,等待网络请求或磁盘读写时 CPU 只能处于空闲等待状态。
需要注意,asyncio 的非阻塞主要针对网络 I/O。它没有原生的非阻塞文件 I/O,普通的文件读写仍会阻塞事件循环,需要借助 asyncio.to_thread 或 aiofiles 等第三方库处理。
asyncio 依托单线程与事件循环(Event Loop)机制工作:
- 事件循环负责在后台轮询和调度任务。
- 协作式多任务:当任务遇到耗时 I/O 并通过 await 挂起时,会主动让出控制权,调度中心立即安排其他就绪的任务执行。
- 适用场景:网络爬虫、Web API 服务、微服务通信等 I/O 密集型任务。若遇到纯计算密集型任务,应当使用 multiprocessing 多进程模块。
二、async 与 await
1. async def 定义协程函数
使用 async def 定义的函数是协程函数。直接调用它不会执行函数体,而是返回一个协程对象(Coroutine Object)。启动异步程序的标准入口是 asyncio.run()。
1 2 3 4 5 6 7 8 9 10 11
| import asyncio
async def fetch_user(): print("开始拉取数据...") await asyncio.sleep(1) print("数据拉取完毕!") return {"id": 101, "name": "Alice"}
result = asyncio.run(fetch_user()) print("返回结果:", result)
|
2. await 的语义与限制
await 的语义是挂起当前协程,让出 CPU 给其他协程运行,待等待的操作完成后再恢复执行。
await 只能写在 async def 函数内部,其后只能接三类可等待对象(Awaitables):
- 协程对象(调用协程函数的返回值)
- Task(通过 asyncio.create_task 包装的对象)
- Future(表示异步操作最终状态的底层对象)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import asyncio
async def step_a(): await asyncio.sleep(1) return "A 完成"
async def step_b(): await asyncio.sleep(1) return "B 完成"
async def main(): res_a = await step_a() print(res_a) res_b = await step_b() print(res_b)
asyncio.run(main())
|
注意:在循环中直接依次 await 协程依然是串行运行的。实现并发需要借助 create_task 或 gather。
三、并发任务管理:asyncio.create_task
asyncio.create_task(coroutine) 用于将协程封装为 Task 对象,并排入事件循环调度。
调用 create_task 后,任务就已排入事件循环,在当前协程让出控制权时开始并发运行,并不需要立刻 await 它。后续对 Task 执行 await,作用是获取其返回值或确认其执行完毕。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| import asyncio import time
async def download(file_name: str, delay: int): print(f"[{file_name}] 开始下载...") await asyncio.sleep(delay) print(f"[{file_name}] 下载完成 (耗时 {delay}s)") return f"{file_name}_data"
async def main(): start = time.perf_counter()
t1 = asyncio.create_task(download("File1", 2)) t2 = asyncio.create_task(download("File2", 3))
print("主流程继续执行其他逻辑...") await asyncio.sleep(0.5)
res1 = await t1 res2 = await t2
print(f"全部完成: {res1}, {res2}") print(f"总耗时: {time.perf_counter() - start:.2f} 秒")
asyncio.run(main())
|
Task 对象的常用操作:
- 取消任务:task.cancel()。被取消的任务在下次 await 时会抛出 asyncio.CancelledError。
- 状态检查:task.done()、task.cancelled()。
- 回调注册:task.add_done_callback(callback_func)。
四、批量结果收集:asyncio.gather
asyncio.gather(*aws, return_exceptions=False) 用于批量聚合多个并发任务。它会自动把传入的协程包装为 Task,并发起调度。返回值严格按照参数传入的先后顺序排列,不会因为某个任务先执行完而打乱顺序。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| import asyncio
async def fetch_api(endpoint: str): await asyncio.sleep(1) if endpoint == "/error": raise ConnectionError("网络波动") return f"Data from {endpoint}"
async def main(): endpoints = ["/users", "/error", "/orders"]
results = await asyncio.gather( *(fetch_api(ep) for ep in endpoints), return_exceptions=True )
for ep, res in zip(endpoints, results): if isinstance(res, Exception): print(f"接口 {ep} 异常: {res}") else: print(f"接口 {ep} 成功: {res}")
asyncio.run(main())
|
参数 return_exceptions 的取值影响:
- False(默认):只要有一个任务抛出异常,gather 会立刻将该异常向外抛出。
- True:异常会被捕获并作为对应位置的结果放入列表,其他任务继续执行不受阻断。
五、create_task 与 gather 的差异
| 维度 |
asyncio.create_task |
asyncio.gather |
| 主要定位 |
单个任务的生命周期控制 |
多个任务的批量执行与结果汇总 |
| 返回值 |
单个 asyncio.Task 对象 |
包含所有结果的 list |
| 调度时机 |
调用时即排入事件循环 |
调用时即排入事件循环,await 时等待全部完成 |
| 控制粒度 |
支持单独 cancel()、查状态或加回调 |
作为整体统一等待或取消 |
| 典型场景 |
后台 Worker 协程、流式任务派发 |
批量请求多个第三方接口并汇总数据 |
六、流式数据处理:async for
当数据按批次流式获取(如分页拉取、大模型逐字推流、日志采集)时,一次性全部载入内存容易造成内存占用过大,应当使用异步迭代器(AsyncIterator)。
异步生成器(async def + yield)
在协程函数中使用 yield,Python 会自动将其封装为异步生成器,省去手动编写迭代器类的工作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import asyncio
async def llm_token_stream(): tokens = ["Deep", "Seek", " / ", "OpenAI", " 正在", "生成", "回答..."] for t in tokens: await asyncio.sleep(0.2) yield t
async def main(): async for token in llm_token_stream(): print(token, end="", flush=True) print()
asyncio.run(main())
|
七、缓冲区与解耦:asyncio.Queue
在生产者消费者的架构中,数据生成和处理的速度往往不一致。asyncio.Queue 是专为单线程内协程设计的先进先出(FIFO)队列。因为运行在单线程事件循环中,它不需要加线程锁,但也不能跨进程传递数据(跨进程需使用 multiprocessing.Queue)。
常用 API:
- queue.put(item):向队列投入元素。若指定了 maxsize 且队列已满,会挂起等待空位。
- queue.get():从队列提取元素。若队列为空,会挂起等待数据。
- queue.task_done():消费者处理完一个元素后调用,通知队列该任务处理完毕。
- queue.join():挂起并等待,直到队列内所有元素都被 get() 且均调用了 task_done()。
- queue.qsize()、empty()、full():非阻塞方式检查队列状态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| import asyncio import random
async def producer(queue: asyncio.Queue, count: int): for i in range(1, count + 1): item = f"Job-{i}" await asyncio.sleep(random.uniform(0.1, 0.3)) await queue.put(item) print(f"[生产者] 生产: {item} (当前队列长度: {queue.qsize()})")
async def consumer(name: str, queue: asyncio.Queue): while True: job = await queue.get() print(f" [{name}] 开始处理: {job}") await asyncio.sleep(random.uniform(0.3, 0.6)) queue.task_done() print(f" [{name}] 处理完毕: {job}")
async def main(): queue = asyncio.Queue(maxsize=3)
consumers = [ asyncio.create_task(consumer(f"Worker-{i}", queue)) for i in range(1, 3) ]
await producer(queue, count=6)
await queue.join() print("[主流程] 所有任务已完成")
for c in consumers: c.cancel() await asyncio.gather(*consumers, return_exceptions=True)
asyncio.run(main())
|
八、完整流水线示例
这个示例综合了前述模块:异步生成器提供流式数据源,asyncio.Queue 控制缓冲与背压,asyncio.create_task 维护工作池,最后用 asyncio.gather 完成收尾。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| import asyncio import time
async def url_stream_generator(total_pages: int): for page in range(1, total_pages + 1): await asyncio.sleep(0.2) yield f"https://api.example.com/data?page={page}"
async def worker(worker_id: int, queue: asyncio.Queue, results: list): while True: url = await queue.get() try: print(f"[Worker-{worker_id}] 开始爬取: {url}") await asyncio.sleep(0.4) results.append({"url": url, "status": 200, "by": worker_id}) finally: queue.task_done()
async def main(): start_time = time.perf_counter() queue = asyncio.Queue(maxsize=5) results = []
worker_tasks = [ asyncio.create_task(worker(i, queue, results)) for i in range(1, 4) ]
print("[调度器] 开始流式投递任务...") async for url in url_stream_generator(total_pages=10): await queue.put(url) print(f"[调度器] 投递成功: {url}")
await queue.join() print("[调度器] 队列清空,任务全部完毕")
for task in worker_tasks: task.cancel() await asyncio.gather(*worker_tasks, return_exceptions=True)
print("\n运行统计:") print(f"成功处理: {len(results)} 条") print(f"总耗时: {time.perf_counter() - start_time:.2f} 秒")
asyncio.run(main())
|
九、常见问题与注意事项
1. 避免在协程中直接调用同步阻塞函数
在 async def 中调用 time.sleep()、requests.get() 或执行耗时的纯 CPU 计算,会阻塞当前主线程,进而使整个事件循环无法调度其他协程。
- 遇到网络 I/O,优先替换为 aiohttp、httpx 等异步库。
- 若必须调用无法修改的同步阻塞函数,使用 asyncio.to_thread 放入内置线程池执行,避免挂起主事件循环:
1 2 3 4 5 6 7
| import time import asyncio
async def main(): res = await asyncio.to_thread(time.sleep, 2)
asyncio.run(main())
|
2. 遗漏 queue.task_done() 导致挂起
每次 put() 一个元素,队列内部的未完成任务计数器就会加 1;消费者 get() 取出元素并处理完成后,必须调用 queue.task_done() 将计数器减 1。一旦遗漏该调用,await queue.join() 的计数永远无法归零,主流程会一直挂起。建议在消费者中将 task_done() 放入 try…finally 块中。