DEBUG.WIKI
京东云;让 AI 进入真实业务;零售 · 物流 · 客服 · 硬件;查看方案 →
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国
Evoxt;最高 6.0GHz CPU;高频云服务器 · 免费每周备份;查看配置 →

Python 报 cannot reuse already awaited coroutine:区分重跑与共享任务

协程对象结束后不能重新等待执行。定时调用用函数或工厂创建新协程,共享同一次执行用 Task,并明确重复副作用、取消、失败缓存与循环生命周期。

第一次 await 正常返回,第二次却报 RuntimeError: cannot reuse already awaited coroutine。常见写法是先把异步函数调用结果存起来,再在定时循环或多次请求中反复等待同一个对象。

本文针对 Python 3.7 及以上的原生 async def 协程和标准 asyncio。报错指向协程对象的生命周期;应先决定业务需要“重新执行一次”,还是“多处等待同一次执行”。

函数可以再次调用,已结束的协程对象不能重跑

下面的示例只演示对象复用问题:

import asyncio

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

async def main():
    operation = load_value()
    first = await operation
    second = await operation  # 已结束的同一个协程对象

if __name__ == "__main__":
    asyncio.run(main())

load_value 是协程函数,load_value() 每次调用创建一个新的协程对象。变量 operation 保存的是某一次调用的执行状态;第一次等待完成后,它不会在下一次 await 时回到函数开头。换一个变量名、复制对象引用或捕获这条异常,都不能让该对象重跑。

如果每次都应重新取值,应在每次等待之前重新调用函数:

async def main():
    first = await load_value()
    second = await load_value()
    return first, second

这段 main 可替换前面脚本中的同名函数。它执行两次业务操作;若函数会付款、发送消息或写数据库,这种修改也意味着两次副作用,不能只为了消除报错就选择重跑。

定时器接收函数或工厂,每轮创建新协程

定时器如果接收 load_value(),得到的仍是一份只能执行一次的协程对象。把参数改成函数或零参数工厂,循环内部再调用:

import asyncio

async def load_value(key):
    await asyncio.sleep(0)
    return {"key": key, "value": 42}

async def repeat(factory, interval, count):
    results = []
    for index in range(count):
        results.append(await factory())
        if index + 1 < count:
            await asyncio.sleep(interval)
    return results

async def main():
    return await repeat(
        lambda: load_value("sample"),
        interval=1.0,
        count=3,
    )

if __name__ == "__main__":
    asyncio.run(main())

这里的 factory() 每轮生成新对象,三个操作依次执行;间隔从上一轮完成后开始计算,因此不是严格按墙钟每秒触发,也不会让多轮重叠。函数有异常时,示例立即向上抛出,不会自动重试。真正的长期定时服务还需要自己决定取消、重试和退出规则。

工厂必须创建新协程。operation = load_value("sample") 后再传 lambda: operation,只是把同一对象包了一层,第二轮仍会失败。

多处共享同一次执行,保存 Task

如果两个调用方需要同一次操作的结果,可以把新协程调度成 Task,并保留这个任务,而不是缓存原始协程对象:

async def main():
    task = asyncio.create_task(load_value("sample"))
    first = await task
    second = await task
    return first, second

这段 main 沿用上面的 load_value(key)。Task 在已有事件循环中管理一次执行,可以重复等待;后一次等待拿到该任务已有的结果,不会再次调用业务函数。结果如果是可变对象,两处拿到的也可能是同一对象,不能因此假定各自拥有独立副本。

失败或取消的任务不会因为再次等待而自动重试。要重试,需明确创建新协程和新任务,并决定是否清掉之前缓存的失败状态。共享 Task 的取消也会影响其他等待者;需要隔离调用方取消时,应另外设计任务所有权,不能把 Task 当作无需管理的永久缓存。

不要跨不同事件循环复用同一任务,例如把第一次 asyncio.run() 内创建的待完成 Task 留给下一次入口。应把共享任务限定在创建它的循环和资源生命周期中,并在结束前处理未完成工作。

排查时沿变量追踪到创建位置:循环外的 operation = async_function()、对象属性里的协程、装饰器或缓存返回的旧协程,都可能把“可再次调用的函数”变成“一次调用的对象”。如果症状是第一次也没执行,并提示 coroutine was never awaited,则应先处理协程没有被等待或调度,两种报错不能用同一条重试逻辑处理。