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

sta*_*ach 6 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")
Run Code Online (Sandbox Code Playgroud)

use*_*342 5

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

Asyncio 并不容易区分。当外部任务等待内部任务时,它将控制委托给内部协程。因此,取消任一任务都会将 aCancelledError注入完全相同的位置:await内部任务的最内部。这就是为什么您无法分辨最初取消了两个任务中的哪一个。

但是,可以通过打破awaits链并使用完成回调连接任务来规避该问题。然后在回调中拦截并检测内部任务的取消:

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
Run Code Online (Sandbox Code Playgroud)

这在功能上等同于await task,除了它不task直接等待内部;它等待一个虚拟的未来,其结果在task完成后设置。在这一点上,我们可以用CancelledError更具体的ChildCancelled. 另一方面,如果外部任务被取消,它将显示为常规CancelledErrorawait 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())
Run Code Online (Sandbox Code Playgroud)

请注意,使用此实现,取消外部任务不会自动取消内部任务,但可以通过显式调用 轻松更改child.cancel(),无论是在 中parent,还是在detect_cancel本身。

Asyncio 使用类似的方法来实现 asyncio.shield().