有时,我的协程清理代码包含一些阻塞部分(在asyncio
意义上,即它们可能会产生)。
我试着仔细设计它们,所以它们不会无限制地阻挡。所以"通过契约",一旦它的清理片段进入协同程序,就不会被中断。
不幸的是,我无法找到防止这种情况的方法,并且当它发生时会发生坏事(无论是由实际的双cancel
电话引起的;还是在它发生时几乎完成了自己,做了清理工作,碰巧从其他地方取消了。)
理论上,我可以将清理委托给其他函数,用shield
保护它,并用try
- except
循环包围它,但它只是丑陋。
是否有Pythonic方法可以这样做?
#!/usr/bin/env python3
import asyncio
@asyncio.coroutine
def foo():
"""
This is the function in question,
with blocking cleanup fragment.
"""
try:
yield from asyncio.sleep(1)
except asyncio.CancelledError:
print("Interrupted during work")
raise
finally:
print("I need just a couple more seconds to cleanup!")
try:
# upload results to the database, whatever
yield from asyncio.sleep(1)
except asyncio.CancelledError:
print("Interrupted during cleanup :(")
else:
print("All cleaned up!")
@asyncio.coroutine
def interrupt_during_work():
# this is a good example, all cleanup
# finishes successfully
t = asyncio.async(foo())
try:
yield from asyncio.wait_for(t, 0.5)
except asyncio.TimeoutError:
pass
else:
assert False, "should've been timed out"
t.cancel()
# wait for finish
try:
yield from t
except asyncio.CancelledError:
pass
@asyncio.coroutine
def interrupt_during_cleanup():
# here, cleanup is interrupted
t = asyncio.async(foo())
try:
yield from asyncio.wait_for(t, 1.5)
except asyncio.TimeoutError:
pass
else:
assert False, "should've been timed out"
t.cancel()
# wait for finish
try:
yield from t
except asyncio.CancelledError:
pass
@asyncio.coroutine
def double_cancel():
# cleanup is interrupted here as well
t = asyncio.async(foo())
try:
yield from asyncio.wait_for(t, 0.5)
except asyncio.TimeoutError:
pass
else:
assert False, "should've been timed out"
t.cancel()
try:
yield from asyncio.wait_for(t, 0.5)
except asyncio.TimeoutError:
pass
else:
assert False, "should've been timed out"
# although double cancel is easy to avoid in
# this particular example, it might not be so obvious
# in more complex code
t.cancel()
# wait for finish
try:
yield from t
except asyncio.CancelledError:
pass
@asyncio.coroutine
def comain():
print("1. Interrupt during work")
yield from interrupt_during_work()
print("2. Interrupt during cleanup")
yield from interrupt_during_cleanup()
print("3. Double cancel")
yield from double_cancel()
def main():
loop = asyncio.get_event_loop()
task = loop.create_task(comain())
loop.run_until_complete(task)
if __name__ == "__main__":
main()
答案 0 :(得分:1)
我最终编写了一个简单的函数,可以提供更强大的屏蔽,可以这么说。
与asyncio.shield
不同,CancelledError
保护被叫者,但在其调用者中引发CancelledError
,此函数完全禁止CancelledError
。
缺点是此功能不允许您稍后处理@asyncio.coroutine
def super_shield(arg, *, loop=None):
arg = asyncio.async(arg)
while True:
try:
return (yield from asyncio.shield(arg, loop=loop))
except asyncio.CancelledError:
continue
。你不会看到它是否曾经发生过。 稍微更复杂的东西需要这样做。
$ ./myscript whatever.txt
答案 1 :(得分:1)
遇到类似问题时,我找到了WGH的解决方案。我想等待一个线程,但是常规的异步取消(有或没有屏蔽)只会取消等待者,并使线程在不受控制的情况下浮动。这是super_shield
的修改,可以选择允许对取消请求做出反应,还可以在等待的范围内处理取消:
await protected(aw, lambda: print("Cancel request"))
这保证了等待的对象已经完成或从内部提出CancelledError
。如果可以通过其他方式取消您的任务(例如,设置线程观察到的标志),则可以使用可选的cancel回调启用取消。
实施:
async def protect(aw, cancel_cb: typing.Callable = None):
"""
A variant of `asyncio.shield` that protects awaitable as well
as the awaiter from being cancelled.
Cancellation events from the awaiter are turned into callbacks
for handling cancellation requests manually.
:param aw: Awaitable.
:param cancel_cb: Optional cancellation callback.
:return: Result of awaitable.
"""
task = asyncio.ensure_future(aw)
while True:
try:
return await asyncio.shield(task)
except asyncio.CancelledError:
if task.done():
raise
if cancel_cb is not None:
cancel_cb()