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")
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.
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.
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.
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.
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 await
s 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()
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With