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,则应先处理协程没有被等待或调度,两种报错不能用同一条重试逻辑处理。


