2016-09-25 114 views
0

有时,我的协程清理代码包含一些阻塞部分(在asyncio的意义上说,即它们可能会产生)。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

'asyncio.shield'是您的问题的推荐方式。 –

+0

@AndrewSvetlov光秃秃的盾牌是不够的,不幸的是。调用'shield'的函数仍然会收到'CancelledError'。 – WGH

回答

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