Python 如果一个任务失败,如何取消聚集中的所有剩余任务?

Python 如果一个任务失败,如何取消聚集中的所有剩余任务?,python,python-3.x,exception,python-asyncio,cancellation,Python,Python 3.x,Exception,Python Asyncio,Cancellation,如果gather的一个任务引发异常,其他任务仍允许继续 嗯,那不正是我需要的。我想区分致命错误和需要取消所有剩余任务的错误,以及在允许其他任务继续执行时不需要记录的错误和应该记录的错误 以下是我实现此功能的失败尝试: 从异步IO导入聚集,获取事件循环,睡眠 应取消其他任务的类错误(异常): 通过 异步定义我的睡眠(秒): 等待睡眠(秒) 如果秒=5: 引发应取消其他任务的错误('5被禁止!') 打印(f'sleep for{secs}secs.) 异步def main(): 尝试: 睡眠者=聚集

如果
gather
的一个任务引发异常,其他任务仍允许继续

嗯,那不正是我需要的。我想区分致命错误和需要取消所有剩余任务的错误,以及在允许其他任务继续执行时不需要记录的错误和应该记录的错误

以下是我实现此功能的失败尝试:

从异步IO导入聚集,获取事件循环,睡眠
应取消其他任务的类错误(异常):
通过
异步定义我的睡眠(秒):
等待睡眠(秒)
如果秒=5:
引发应取消其他任务的错误('5被禁止!')
打印(f'sleep for{secs}secs.)
异步def main():
尝试:
睡眠者=聚集(*[2,5,7]中我的睡眠(秒)为秒)
等待睡眠者
除应取消其他任务的错误外:
打印('致命错误;正在取消')
1.取消
最后:
等待睡眠(5)
获取\u事件\u循环()。运行\u直到完成(main())
(此处的
最终等待睡眠
是为了防止解释器立即关闭,这将自动取消所有任务)

奇怪的是,在
collect
上调用
cancel
实际上并没有取消它

PS C:\Users\m> .\AppData\Local\Programs\Python\Python368\python.exe .\wtf.py
Slept for 2secs.
Fatal error; cancelling
Slept for 7secs.
我对这种行为感到非常惊讶,因为它似乎与以下内容相矛盾:

asyncio.gather(*coros\u或\u futures,loop=None,return\u exceptions=False)

从给定的协程对象或未来返回未来聚合结果

(……)

取消:如果外部未来被取消,所有子项(尚未完成)也将被取消。(……)


我错过了什么?如何取消剩余的任务?

您的实现的问题是,它在
sleers
已引发后调用
sleers.cancel()
。从技术上讲,
gather()
返回的future处于已完成状态,因此其取消必须为no-op

要更正代码,您只需自己取消子项,而不必信任
gather
的未来。当然,协同程序本身是不可取消的,因此您需要首先将其转换为任务(不管怎样,
gather
都可以,所以您不需要做额外的工作)。例如:

async def main():
    tasks = [asyncio.ensure_future(my_sleep(secs))
             for secs in [2, 5, 7]]
    try:
        await asyncio.gather(*tasks)
    except ErrorThatShouldCancelOtherTasks:
        print('Fatal error; cancelling')
        for t in tasks:
            t.cancel()
    finally:
        await sleep(5)
我对这种行为感到非常惊讶,因为它似乎与文档相矛盾[…]

使用
gather
的最初障碍是它并没有真正运行任务,它只是一个等待任务完成的助手。因此,
gather
如果其中一些任务因异常而失败,则不必费心取消其余任务-它只是放弃等待并传播异常,将其余任务留在后台继续。这是,但不是为了向后兼容而修复的,因为行为从一开始就被记录下来并保持不变。但这里我们还有另一个缺点:文档明确承诺能够取消返回的未来。您的代码正是这样做的,但这不起作用,原因还不清楚(至少我花了一段时间才弄明白,并且需要阅读该文档)。事实证明,合同实际上阻止了这一点。当您调用
cancel()
时,
gather
返回的未来已经完成,取消一个已完成的未来是没有意义的,它只是禁止操作。(原因是一个已完成的期货有一个定义良好的结果,外部代码可能已经观察到了。取消它将改变其结果,这是不允许的。)

换句话说,文档并没有错,因为如果您在
await sleers
完成之前执行了取消操作,那么取消操作就会起作用。然而,这是一种误导,因为它似乎允许在一个重要用例中取消
gather()
,而实际上却不允许

使用
gather
时会出现这样的问题,这是许多人热切期待(并非双关语)三位一体式托儿所的原因。

您可以创建自己的自定义聚集功能 当发生任何异常时,将取消其所有子项:

导入异步IO
异步def聚集(*任务,**kwargs):
tasks=[task if is instance(task,asyncio.task)else asyncio.create_task(task)
对于任务中的任务]
尝试:
return wait asyncio.gather(*任务,**kwargs)
除BaseException作为e外:
对于任务中的任务:
task.cancel()
提高e
#如果a()或b()引发异常,则这两个异常都将立即取消
a_result,b_result=等待聚集(a(),b())

非常感谢!需要解决的另一个问题是,如果一个coro引发了一个非致命错误,那么另一个coro随后引发的致命错误将被忽略;我想答案是将
my\u sleep
的实现包装在一个(否则是不明智的)
try:…除非异常为err:log\u Exception\u和\u ignore\u it(err)
?@gaazkam如果您的需求是继续,这可能是最简单的方法。还可以选择将
聚集
打包成一个循环,捕获
异常除外
并修剪引发的任务(并记录其异常),但结果只是比您建议的代码多得多,而且没有真正的收益。我需要做的一件事是避免从所有
任务中引发取消错误。cancel()
调用是通过等待另一个gather语句,即
wait asyncio.gather,来跟踪任务中t的
循环(*任务,返回_exceptions=True)