asyncio.as_completed无法监听进度,因其仅按完成顺序返回最终结果,不暴露中间状态;进度需由任务自身通过asyncio.Queue等机制主动上报,与as_completed协同工作。

asyncio.as_completed 为什么不能直接监听进度
asyncio.as_completed 的设计目标是“按完成顺序返回 Task 结果”,它本身不暴露中间状态或进度值。你调用 await 它时,拿到的是一个接一个的最终结果,不是“10% → 50% → 100%”这种流式反馈。想靠它原生支持进度监听,会发现根本没地方塞回调、也没进度字段可读。
常见错误现象:写了个循环 for coro in asyncio.as_completed(tasks): result = await coro,结果所有任务都跑完了才开始处理,完全无法感知某个任务卡在哪儿、耗时多久。
- 它返回的是
Future对象(不是协程),且只在完成时才可await - 没有内置钩子或事件机制,无法插入进度更新逻辑
- 若任务内部无主动上报,
as_completed就是“黑盒等结果”
真正可行的进度监听必须由任务自身驱动
进度不是调度器给的,是你自己在协程里控制和暴露的。核心思路是:让每个异步任务定期通过共享对象(如 dict、asyncio.Queue 或 threading.Event)广播当前状态,主协程一边用 as_completed 等结果,一边轮询或监听这些状态通道。
推荐做法是用 asyncio.Queue 做进度中转——轻量、线程安全、天然适配协程:
立即学习“Python免费学习笔记(深入)”;
progress_q = asyncio.Queue()
# 每个任务启动时传入这个 queue
async def long_task(task_id, progress_q):
for i in range(100):
# 模拟工作
await asyncio.sleep(0.05)
# 主动上报进度
await progress_q.put({"task_id": task_id, "progress": i + 1})
await progress_q.put({"task_id": task_id, "done": True, "result": f"ok-{task_id}"})主协程用 asyncio.create_task 启动监听消费者,和 as_completed 并行跑:
- 不要阻塞等待
progress_q.get(),要用asyncio.wait_for(..., timeout=0.1)防卡死 -
as_completed仍用于捕获最终结果,但别让它独占控制流 - 避免在任务里频繁
put小数据(比如每毫秒一次),会拖慢整体吞吐
用 asyncio.Queue + as_completed 协同工作的典型结构
这不是二选一,而是分工:一个管“谁先做完”,一个管“正在干啥”。关键是要让两者不互相干扰,也不漏消息。
实操要点:
- 启动所有任务时,统一传入同一个
progress_q实例 - 用
asyncio.create_task单独起一个消费者协程,持续get()进度并分发(比如打印、存 DB、推 WebSocket) -
as_completed循环只负责收结果,收到后可以查本地缓存确认该任务是否已上报过 95% 进度,做一致性校验 - 如果任务可能失败,记得在
except块里也put一条失败状态,否则主监听会永远等不到它的“done”
参数差异注意:asyncio.Queue 默认无界,高并发下建议设 maxsize=100 防内存爆;而 as_completed 接收的是 Task 列表,不是 coro 列表,务必用 asyncio.create_task(coro) 包一层再传。
替代方案对比:什么时候不该用 as_completed
如果你的核心诉求是“实时响应任意任务的任意阶段变化”,而不是“按完成顺序收结果”,那 as_completed 就是错的抽象层。此时更合适的是:
-
asyncio.gather(..., return_exceptions=True)+ 手动遍历Task的done()/cancelled()状态轮询 - 用
asyncio.Condition配合状态字典,让任务完成/进度更新时notify_all - 重度监控场景直接上
trio或anyio,它们对结构化取消和状态传播支持更自然
性能影响明显:轮询 Task.done() 每 10ms 一次基本无压力;但用 as_completed + 频繁 Queue.get() 组合,在千级任务时要注意 Queue 的内部锁竞争。
容易被忽略的一点:所有进度上报都应带唯一 task_id,且最好在任务启动时就生成(比如用 uuid.uuid4().hex[:8]),别依赖 Task.get_coro().__name__ —— 多个相同函数实例会撞名。


















