Python 使用create_任务创建的任务从未等待,似乎打破了取消子任务的期望
假设我们正在编写一个应用程序,该应用程序允许用户连续运行一个应用程序(假设这是针对API的一系列重要操作),并且可以并发运行多个应用程序。要求包括:Python 使用create_任务创建的任务从未等待,似乎打破了取消子任务的期望,python,python-asyncio,Python,Python Asyncio,假设我们正在编写一个应用程序,该应用程序允许用户连续运行一个应用程序(假设这是针对API的一系列重要操作),并且可以并发运行多个应用程序。要求包括: 用户可以控制并发应用程序的数量(这可能会限制API的并发负载,这通常很重要) 如果操作系统试图关闭运行这个东西的Python程序,它应该正常终止,允许任何正在运行的应用程序在关闭之前完成运行 这里的问题是关于我们编写的任务管理器的,所以让我们去掉一些说明此问题的代码: import asyncio import signal async d
- 用户可以控制并发应用程序的数量(这可能会限制API的并发负载,这通常很重要)
- 如果操作系统试图关闭运行这个东西的Python程序,它应该正常终止,允许任何正在运行的应用程序在关闭之前完成运行
import asyncio
import signal
async def work_chunk():
"""Simulates a chunk of work that can possibly fail"""
await asyncio.sleep(1)
async def protected_work():
"""All steps of this function MUST complete, the caller should shield it from cancelation."""
print("protected_work start")
for i in range(3):
await work_chunk()
print(f"protected_work working... {i+1} out of 3 steps complete")
print("protected_work done... ")
async def subtask():
print("subtask: starting loop of protected work...")
cancelled = False
while not cancelled:
protected_coro = asyncio.create_task(protected_work())
try:
await asyncio.shield(protected_coro)
except asyncio.CancelledError:
cancelled = True
await protected_coro
print("subtask: cancelation complete")
async def subtask_manager():
"""
Manage a pool of subtask workers.
(In the real world, the user can dynamically change the concurrency, but here we'll
hard code it at 3.)
"""
tasks = {}
while True:
for i in range(3):
task = tasks.get(i)
if not task or task.done():
tasks[i] = asyncio.create_task(subtask())
await asyncio.sleep(5)
def shutdown(signal, main_task):
"""Cleanup tasks tied to the service's shutdown."""
print(f"Received exit signal {signal.name}. Scheduling cancelation:")
main_task.cancel()
async def main():
print("main... start")
coro = asyncio.ensure_future(subtask_manager())
loop = asyncio.get_running_loop()
loop.add_signal_handler(signal.SIGINT, lambda: shutdown(signal.SIGINT, coro))
loop.add_signal_handler(signal.SIGTERM, lambda: shutdown(signal.SIGTERM, coro))
await coro
print("main... done")
def run():
asyncio.run(main())
run()
subtask\u manager
管理一组工作人员,定期查找当前的并发要求,并适当地更新活动工作人员的数量(注意,上面的代码删去了其中的大部分,只需硬编码一个数字,因为这对问题并不重要)
子任务
是工作者循环本身,它持续运行受保护的工作()
,直到有人取消它
但是这个密码被破解了。当你给它一个信号,整个事情立刻崩溃
在我进一步解释之前,让我向您指出一段关键代码:
1 protected_coro = asyncio.create_task(protected_work())
2 try:
3 await asyncio.shield(protected_coro)
4 except asyncio.CancelledError:
5 cancelled = True
6 await protected_coro # <-- This will raise CancelledError too!
1 protected\u coro=asyncio.create\u任务(protected\u work())
2.尝试:
3等待异步屏蔽(受保护)
4除asyncio.Cancelled错误外:
5=真
6 wait protected_coro#在研究了这个问题很长一段时间后,并对其他代码片段进行了实验(取消传播如预期的那样工作),我开始怀疑问题是否在于Python不知道这里的传播顺序,在这种情况下
但是为什么呢
嗯,子任务管理器创建任务,但不等待它们
难道Python不认为创建该任务(与create\u task
)的协同程序拥有该任务吗?我认为Python专门使用wait
关键字来知道以什么顺序传播取消,如果在遍历整个任务树之后,它发现仍然没有取消的任务,它只会将它们全部销毁
因此,在我们知道没有等待异步任务的任何地方,我们都可以自己管理任务取消传播。因此,我们需要重构subtask\u manager
以捕获其自身的取消,并显式取消,然后等待其所有子任务:
async def subtask_manager():
"""
Manage a pool of subtask workers.
(In the real world, the user can dynamically change the concurrency, but here we'll
hard code it at 3.)
"""
tasks = {}
while True:
for i in range(3):
task = tasks.get(i)
if not task or task.done():
tasks[i] = asyncio.create_task(subtask())
try:
await asyncio.sleep(5)
except asyncio.CancelledError:
print("cancelation detected, canceling children")
[t.cancel() for t in tasks.values()]
await asyncio.gather(*[t for t in tasks.values()])
return
现在,我们的代码按预期工作:
注意:我已经以问答的方式回答了我自己的问题,但我仍然对我关于取消传播工作原理的文本回答感到不满意。如果有人对取消传播的工作原理有更好的解释,我很乐意阅读
这是怎么回事?由于某种原因,Python似乎并没有像您所期望的那样传播取消,只是举手说“我现在正在取消一切”
TL;DR取消一切正是正在发生的事情,因为事件循环正在退出
为了研究这一点,我将add\u signal\u handler()
的调用更改为loop.call\u later(.5,lambda:shutdown(signal.SIGINT,coro))
。Python的Ctrl+C处理已经完成,我想检查这种奇怪的行为是否是这种情况的结果。但是这个错误在没有信号的情况下是完全可以复制的,所以不是这样
然而,异步IO取消确实不应该像代码显示的那样工作。取消一个任务会传播到它等待的未来(或另一个任务),但是专门实现了shield
来规避这一点。它创建并返回一个新的未来,并以cancel()
不知道如何遵循的方式将原始(屏蔽)未来的结果连接到新的未来
我花了一些时间才发现真正发生的事情,那就是:
await coro
在main的末尾等待被取消的任务,因此只要shutdown
取消它,它就会得到一个cancelederror
- 异常导致退出
main
,并在asyncio.run()的末尾进入清理序列。此清理序列取消所有任务,包括您屏蔽的任务
您可以将main()
末尾的wait coro
更改为:
试试看:
等待科罗
最后:
打印('main…done')
你会看到,在你目睹的所有神秘的取消之前,“主要…完成”已经打印出来
为了解开谜团并解决问题,您应该推迟退出main
,直到一切都完成。例如,您可以在main
中创建任务dict,将其传递给子任务管理器()
,然后在取消主任务时等待这些关键任务:
async def子任务管理器(任务):
尽管如此:
对于范围(3)中的i:
任务=任务。获取(i)
如果不是task或task.done():
tasks[i]=asyncio.create_任务(子任务())
尝试:
等待异步睡眠(5)
除asyncio.Cancelled错误外:
对于tasks.values()中的t:
t、 取消
提升
# ... 停产不变
异步def main():
打印(“主…开始”)
任务={}
主任务=异步。确保未来(子任务管理器(任务))
loop=asyncio.get\u running\u loop()
添加信号处理器(signal.SIGINT,lambda:shutdown(signal.SIGINT,main_任务))
添加信号处理程序(signal.SIGTERM,lambda:shutdown(signal.SIGTERM,main任务))
尝试:
等待主要任务
除asyncio.Cancelled错误外:
等待asyncio.gather(*tasks.values())
最后:
打印(“主。。。