asyncio.wait_for() 不能直接套在 asyncio.gather() 外面,否则超时后子协程不会被取消而成为“幽灵任务”;正确做法是用 asyncio.timeout() + asyncio.create_task() + gather(return_exceptions=true) 统一管理取消信号。

asyncio.wait_for() 不能直接套在 asyncio.gather() 外面?
很多人第一反应是 asyncio.wait_for(asyncio.gather(...), timeout=10),但这样会出问题:一旦超时,wait_for 抛出 TimeoutError,而 gather 内部的协程可能还在运行——它们不会被自动取消,变成“幽灵任务”,持续占用资源甚至引发状态错乱。
正确做法是让超时真正作用于所有子任务的生命周期。关键不是“包一层”,而是“统一注入取消信号”。
- 必须用
asyncio.create_task()显式创建每个子任务,而非直接传给gather - 所有任务需共享同一个
asyncio.Timeout上下文,或手动监听取消信号 -
gather的return_exceptions=True参数要设为True,否则一个任务超时会导致整个gather提前退出,其他任务没机会响应取消
推荐方案:用 asyncio.timeout() + create_task() + gather(return_exceptions=True)
Python 3.11+ 原生支持 asyncio.timeout(),它是目前最干净、语义最明确的方式。它会在超时时刻自动取消所有在该上下文中启动的 task(前提是这些 task 真正响应取消)。
import asyncio
<p>async def fetch(url):
await asyncio.sleep(2) # 模拟网络请求
return f"done: {url}"</p><p>async def batch_fetch(urls):
tasks = []
async with asyncio.timeout(5): # 全局 5 秒超时
for url in urls:
tasks.append(asyncio.create_task(fetch(url)))
results = await asyncio.gather(*tasks, return_exceptions=True)
return results</p><h1>调用</h1><p>results = asyncio.run(batch_fetch(["a", "b", "c"]))
</p>
注意:asyncio.timeout() 只对“在它作用域内创建的 task”生效。如果你在 timeout 块外调用了 create_task(),它不会被取消。
兼容旧版本(
Python 3.9–3.10 可用 asyncio.wait_for() 配合显式 task 创建和异常处理;3.8 及更早需自己管理 asyncio.shield() 和 task.cancel()。核心逻辑不变:先创建 task,再统一等待,出错后手动 cancel 所有未完成 task。
- 不要用
wait_for(gather(...)),改用wait_for(asyncio.gather(*tasks), ...),且 tasks 必须是create_task()返回的对象 - 捕获
TimeoutError后,遍历所有 task,对not task.done()的调用task.cancel() - 最后仍要用
gather(..., return_exceptions=True)等待它们真正结束,避免 pending task 泄漏
示例(3.9+):
async def batch_fetch_legacy(urls):
tasks = [asyncio.create_task(fetch(u)) for u in urls]
try:
return await asyncio.wait_for(
asyncio.gather(*tasks, return_exceptions=True),
timeout=5
)
except asyncio.TimeoutError:
for t in tasks:
if not t.done():
t.cancel()
# 等待取消完成,忽略 CancelledError
await asyncio.gather(*tasks, return_exceptions=True)
raise
为什么有些协程不响应取消?常见陷阱
即使你用了 timeout 或 cancel(),如果某个协程内部用了阻塞调用(如 time.sleep()、未加 await 的 requests.get())、没做取消检查、或在 try/except BaseException 中吞掉了 CancelledError,它就会卡住不动。
- 永远用
await asyncio.sleep()替代time.sleep() - HTTP 请求务必用异步库(
aiohttp、httpx.AsyncClient),别混用requests - 长时间循环中定期写
if asyncio.current_task().cancelled(): raise asyncio.CancelledError(虽然通常 await 点已隐含检查) - 避免
except Exception:或except BaseException:,至少要重新抛出CancelledError
全局超时是否生效,最终取决于每个子协程是否“可取消”。这不是语法问题,而是协作契约。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











