Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

distinguish between cancellation of shielded task and current task

While reading: https://docs.python.org/3/library/asyncio-task.html#asyncio.Task.cancel it seems that catching CancelledError is used for two purposes.

One is potentially preventing your task from getting cancelled.

The other is determining that something has cancelled the task you are awaiting. How to tell the difference?

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")
like image 752
stach Avatar asked Mar 28 '19 22:03

stach


People also ask

When_all() and when_any() cancel when a task is canceled?

When you provide a cancellation token to either the when_all and when_any function, that function cancels only when that cancellation token is canceled or when one of the participant tasks ends in a canceled state or throws an exception.

What is task cancellation mechanism?

Task cancellation mechanism is an easy and useful tool for controlling task execution flow. It is a part of big .NET Framework ecosystem, therefore its usage in most cases is more preferable than some custom solutions.

How to cancel multiple tasks at the same time?

In addition, you must pass the same cancellation token to the constructor of any nested tasks (that is, tasks that are created in the body of another task) to cancel all tasks simultaneously. You might want to run arbitrary code when a cancellation token is canceled.

What are the disadvantages of Task cancellation?

For example, because task cancellation is cooperative, the overall set of tasks will not cancel if any individual task is blocked. For example, if one task has not yet started, but it unblocks another active task, it will not start if the task group is canceled. This can cause deadlock to occur in your application.


1 Answers

How to tell the difference [between ourselves getting canceled and the task we're awaiting getting canceled]?

Asyncio doesn't make it easy to tell the difference. When an outer task awaits an inner task, it is delegating control to inner one's coroutine. As a result, canceling either task injects a CancelledError into the exact same place: the inner-most await inside the inner task. This is why you cannot tell the which of the two tasks was canceled originally.

However, it is possible to circumvent the issue by breaking the chain of awaits and connecting the tasks using a completion callback instead. Cancellation of the inner task is then intercepted and detected in the callback:

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

This is functionally equivalent to await task, except it doesn't await the inner task directly; it awaits a dummy future whose result is set after task completes. At this point we can replace the CancelledError (which we know must have come from cancellation of the inner task) with the more specific ChildCancelled. On the other hand, if the outer task is cancelled, that will show up as a regular CancelledError at await cont, and will be propagated as usual.

Here is some test code:

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

Note that with this implementation, cancelling the outer task will not automatically cancel the inner one, but that may be easily changed with an explicit call to child.cancel(), either in parent, or in detect_cancel itself.

Asyncio uses a similar approach to implement asyncio.shield().

like image 120
user4815162342 Avatar answered Oct 22 '22 13:10

user4815162342