Python asyncio.Future 无显式操作时自行取消抛出CancelledError问题
问题根本原因
这个异常是PENDING状态的asyncio.Future被垃圾回收时自动触发取消导致的,你没有手动调用cancel、事件循环也未关闭却触发异常,完全是代码逻辑bug导致Future失去有效强引用、永远无法被设置结果:
- 第一处核心错误:你在
hold方法入口就执行pop操作,会把已存在的待处理请求从__held_requests中提前移除。正常情况下你的业务逻辑会遍历__held_requests找到待处理的请求执行、再给对应Future设置结果,提前pop会导致处理逻辑找不到这个请求,对应的Future永远不会被执行set_result()/set_exception()。 - 第二处核心错误:你混用了存储结构API:从
pop(key, default)的写法可以判断__held_requests应该是字典(Python原生set的pop方法不接收参数,传参会直接抛TypeError),但你存入新请求时用了set专属的add()方法,这会导致新创建的RatingRequestPerishable和Future根本没有被正确存入存储结构。就算你用了自定义支持按key弹出的set,也存在竞态漏洞:并发场景下多个协程会同时检测到key不存在,各自创建新Future,只有最后一个创建的实例会被存入集合,之前创建的Future不会被处理逻辑感知。
Python会在运行时自动分析局部变量生命周期,一个变量如果后续不再被访问,会被提前释放引用。当PENDING状态的Future没有任何强引用、被GC回收时,asyncio会自动调用它的cancel()方法,抛出你看到的asyncio.exceptions.CancelledError。
你观察到的“每隔一次运行触发异常”的现象和这个bug完全匹配:第一次调用创建的Future被正确持有,可以正常等待结果返回;第二次调用要么提前把已存的Future弹出导致引用丢失,要么创建的新Future没被正确存入存储结构,最终被GC自动取消。
修复方案
- 统一
__held_requests为普通字典类型,key为request_key,value为对应的RatingRequestPerishable实例,存入新请求时用字典赋值语法:self.__held_requests[request_key] = rating_perishable,不要用add()方法。 - 移除方法入口的pop逻辑,改为先查询key是否存在:如果存在直接取对应Future等待;不存在再创建新实例存入字典。
- 只有当请求处理完成、已经给对应Future设置完结果/异常后,再把key从字典中删除,禁止提前移除待处理项。
- 高并发场景下,给字典的检查、创建、写入操作加异步锁,避免竞态条件导致重复创建Future。
修复后的核心代码参考:
async def hold(self, request_key): logger.info(f"HOLDING: request_key={request_key}") # 仅查询,不提前弹出元素 ongoing_rating_perishable = self.__held_requests.get(request_key, None) if ongoing_rating_perishable is None: future = asyncio.get_running_loop().create_future() rating_perishable = RatingRequestPerishable(request_key, future, holding=True) # 字典用赋值语法存储 self.__held_requests[request_key] = rating_perishable else: future = ongoing_rating_perishable.rating_future try: print(f"state: {future._state}") result = await future except asyncio.CancelledError as err: print(traceback.format_exc()) # 异常时清理无效缓存 if request_key in self.__held_requests and self.__held_requests[request_key].rating_future is future: del self.__held_requests[request_key] raise return result # 业务逻辑处理完成后调用该方法设置结果、清理缓存 def release(self, request_key, result): perishable = self.__held_requests.pop(request_key, None) if perishable and not perishable.rating_future.done(): perishable.rating_future.set_result(result)
内容的提问来源于stack exchange,提问作者LetsDoThis
相关产品推荐
相关产品推荐

