asyncio:阻止任务被取消两次

时间:2016-09-25 14:38:53

标签: python python-asyncio coroutine

有时,我的协程清理代码包含一些阻塞部分(在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()

2 个答案:

答案 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()