区分取消屏蔽任务和当前任务

时间:2019-03-28 22:45:46

标签: python python-asyncio

在阅读时: https://docs.python.org/3/library/asyncio-task.html#asyncio.Task.cancel 似乎捕获CancelledError有两个目的。

一个有可能阻止您取消任务。

另一个正在确定某件事取消了您正在等待的任务。 如何区分?

async def cancel_me():
    try:
        await asyncio.sleep(3600)
    except asyncio.CancelledError:
        raise
    finally:
        print('cancel_me(): after sleep')

async def main():
    task = asyncio.create_task(cancel_me())
    await asyncio.sleep(1)
    task.cancel()
    try:
        await task
    except asyncio.CancelledError:
        # HERE: How do I know if `task` has been cancelled, or I AM being cancelled?
        print("main(): cancel_me is cancelled now")

2 个答案:

答案 0 :(得分:3)

  

如何分辨[我们自己被取消和我们正在等待被取消的任务之间的区别]?

Asyncio很难说出区别。当外部任务等待内部任务时,它将控制权委派给内部人员的协程。结果,取消任一个任务都会将CancelledError注入到完全相同的位置:内部任务中最里面的await。这就是为什么您无法确定两个任务中的哪个最初被取消了。

但是,可以通过断开await的链并使用完成回调来连接任务来解决此问题。然后在回调中拦截并检测到内部任务的取消:

class ChildCancelled(asyncio.CancelledError):
    pass

async def detect_cancel(task):
    cont = asyncio.get_event_loop().create_future()
    def on_done(_):
        if task.cancelled():
            cont.set_exception(ChildCancelled())
        elif task.exception() is not None:
            cont.set_exception(task.exception())
        else:
            cont.set_result(task.result())
    task.add_done_callback(on_done)
    await cont

这在功能上等效于await task,只是它不直接等待内部task;它等待一个伪未来,其结果在task完成后设置。此时,我们可以用更具体的CancelledError来代替ChildCancelled(我们知道它必须来自取消内部任务)。另一方面,如果取消了外部任务,则该任务将在CancelledError处以常规await cont的形式显示,并照常传播。

以下是一些测试代码:

import asyncio, sys

# async def detect_cancel defined as above

async def cancel_me():
    print('cancel_me')
    try:
        await asyncio.sleep(3600)
    finally:
        print('cancel_me(): after sleep')

async def parent(task):
    await asyncio.sleep(.001)
    try:
        await detect_cancel(task)
    except ChildCancelled:
        print("parent(): child is cancelled now")
        raise
    except asyncio.CancelledError:
        print("parent(): I am cancelled")
        raise

async def main():
    loop = asyncio.get_event_loop()
    child_task = loop.create_task(cancel_me())
    parent_task = loop.create_task(parent(child_task))
    await asyncio.sleep(.1)  # give a chance to child to start running
    if sys.argv[1] == 'parent':
        parent_task.cancel()
    else:
        child_task.cancel()
    await asyncio.sleep(.5)

asyncio.get_event_loop().run_until_complete(main())

请注意,通过此实现,取消外部任务不会自动取消内部任务,但是可以通过在child.cancel()或{{ 1}}本身。

Asyncio使用类似于implement asyncio.shield()的方法。

答案 1 :(得分:0)

上下文

首先,让我们考虑更广泛的背景:

caller() --> your_coro() --> callee()

您可以控制协程,但不能控制调用者,而只能部分控制被调用者。

默认情况下,取消有效地通过堆栈的上下方向“传播”:

(1)
caller1() ------------------+    (2)
                            +--> callee()
caller2() --> your_coro() --+
(4)          (3)

在此图中,从语义上和非常宽松,如果caller1()被主动取消,则callee()被取消,然后协程被取消,然后{{1} }被取消。如果主动取消caller2(),情况大致相同。

({caller2()是共享的,因此不是普通的协程,而是callee()Task

您可能想要什么替代行为

如果您希望Future继续执行,即使callee()被取消,也可以caller2()

shield

反屏蔽

如果您允许callee_f = asyncio.ensure_future(callee()) async def your_coro(): # I might die, but I won't take callee down with me await asyncio.shield(callee_f) 死亡,但希望您的协程继续进行,请转换例外

callee()

保护自己

这是一个问题-通常,您应允许呼叫者取消协程。

一个值得注意的例外是,如果您的调用方是一个框架,并且它是不可配置的。

async def reverse_shield(awaitable):
    try:
        return await awaitable
    except asyncio.CancelledError:
        raise Exception("custom")

async def your_coro():
    await reverse_shield(callee_f)
    # handle custom exception