DEBUG.WIKI
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国
雨云;给项目一台云服务器
RackNerd;$21.99/年 · 1GB KVM

asyncio.wait 报 Passing coroutines is forbidden,先检查传入对象

Python 3.11 起 asyncio.wait 不接受裸协程。正确创建并持有 Task,处理 done、pending 与取消,核对第三方库已发布修复。

升级 Python 后,原来的 asyncio.wait([task, event.wait()]) 报 TypeError: Passing coroutines is forbidden, use tasks explicitly.。列表里看起来都是能等待的对象,但 event.wait() 返回裸协程,不能直接交给 Python 3.11 及以上的 asyncio.wait。

需要修的是输入对象和任务生命周期。补一个 await,或随手把协程全部启动后不再管理,都可能改变原来“等待任意一个先完成”的逻辑。

找到列表里尚未调度的协程

async def 函数调用后的对象、asyncio.Event.wait() 等调用结果可能是协程;已经创建的 Task 和 Future 则是另外两种对象。wait 要求非空的 Task/Future 集合,不能把“可以 await”直接当作其输入要求。

自己创建的协程可以在正在运行的事件循环里用 asyncio.create_task() 调度,并保存返回的 Task。已经是 Future 的对象不用再次传给 create_task;后者接收的是协程,不是任意可等待对象。

如果 traceback 指向第三方库,先核对库版本。huggingface_hub 0.32.3 的 tiny-agents 输入等待路径曾传入裸 exit_event.wait();0.32.4 已加入对应的 Task 创建修复。受影响的旧环境应按项目依赖约束更新到包含修复的兼容版本,不需要修改 site-packages。这项版本边界不能用于其他库的同名错误,也不意味着最新版本还存在同一缺陷。

wait 返回 pending,不负责替你关闭任务

wait(..., timeout=...) 超时后返回 done 和 pending,不会自动抛出 TimeoutError,也不会自动取消 pending。FIRST_COMPLETED 返回时,其他任务同样可能仍在执行;外层等待被取消,也不代表输入 Task 全部结束。

下面演示自己拥有两个新任务、决定返回前停止剩余任务的结构。它使用 Python 3.11 及以上,load_value 只是可取消的异步示例,不涉及线程或真实外部操作:

import asyncio


async def load_value():
    await asyncio.sleep(1)
    return 42


async def wait_for_owned_work(exit_event):
    work_task = asyncio.create_task(load_value())
    exit_task = asyncio.create_task(exit_event.wait())
    tasks = {work_task, exit_task}
    try:
        done, pending = await asyncio.wait(
            tasks,
            timeout=5,
            return_when=asyncio.FIRST_COMPLETED,
        )
        if not done:
            raise TimeoutError("work did not finish within 5 seconds")
        if work_task in done:
            return work_task.result()
        exit_task.result()
        return None
    finally:
        for task in tasks:
            if not task.done():
                task.cancel()
        await asyncio.gather(*tasks, return_exceptions=True)

超时异常由这段业务代码主动抛出;wait 本身只返回集合。已完成任务用 result() 取回业务结果或抛出其异常。finally 取消并等待自己拥有的任务,避免只调用 cancel() 就退出,把资源清理留在后台。

这里没有吞掉调用方的 CancelledError。任务若自行压住取消,清理也可能继续等待;实际 I/O 应有合理超时和退出机制。同步线程里的工作另有生命周期,不能用这段示例保证它被强制停止。

借来的 Task 要保留原来的所有权

任务来自连接管理者、缓存或其他调用方时,不能照搬上面“全部取消”的流程。共享任务可能还有其他等待者,应先决定谁有权取消,谁继续收取结果和异常。若需要让未完成任务继续,必须保留强引用并交给明确的管理者,不能只丢弃 pending。

还要检查原来使用的是 FIRST_COMPLETED、FIRST_EXCEPTION 还是默认全部完成。改成 gather 会改变返回和失败行为;在列表生成式里逐个 await 又可能变成串行执行。先保留业务需要的等待条件,再修正对象类型。

在隔离测试中分别检查正常完成、退出事件先到、超时和调用方取消。每个结果都应有对应处理,自己拥有的残留任务和资源应完成清理。修改前保留原等待及取消逻辑,回退时也要核对已经启动的任务,不能用重新创建协程掩盖还在执行的旧工作。