Why is the CancelledError
not caught in this example?
import asyncio
q = asyncio.Queue()
async def getter():
try:
v = await q.get()
print(f"getter got {v}")
except asyncio.CancelledError:
print("getter cancelled")
async def test():
task = asyncio.ensure_future(getter())
task.cancel()
await task
def main():
loop = asyncio.get_event_loop()
loop.run_until_complete(test())
if __name__ == '__main__':
main()
I excpected to get the "getter cancelled" message, but received a stack trace instead:
Traceback (most recent call last): File "ce.py", line 22, in main() File "ce.py", line 19, in main loop.run_until_complete(test()) File "/usr/lib64/python3.6/asyncio/base_events.py", line 468, in run_until_complete return future.result() concurrent.futures._base.CancelledError
Task.cancel states:
This arranges for a CancelledError to be thrown into the wrapped coroutine on the next cycle through the event loop. The coroutine then has a chance to clean up or even deny the request using try/except/finally.
The problem is that getter
didn't even start executing, which you can confirm by adding a print at its beginning. Since the try
block was never entered, the except
didn't run either.
This happens because, in contrast to await
, ensure_future
doesn't start executing the coroutine right away, it just schedules it to run at the next event loop iteration, like call_soon
does for ordinary functions. Since you cancel the task immediately, it gets removed from the runnable set and its coroutine gets closed without ever having started.
Add an await asyncio.sleep(0)
before task.cancel()
and you should observe the behavior you expect. I suspect you don't need to make such change in your actual code - in the unlikely case that the task gets cancelled before it ran, as in the example, it won't get a chance to acquire the resources that try/except cleans up in the first place.
Two tangential remarks:
You probably want to re-raise asyncio.CancelledError
after handling it, otherwise it will be suppressed. That's not a problem in getter
as shown in the question, but it could be an issue if the code were buried in a function call. Even better, consider using finally
or with
, which propagate the exception and ensure that the resources are released regardless of exception type.
When you need to create a task and run a coroutine, loop.create_task
is preferred to asyncio.ensure_future
. In short, although both do the same thing for coroutines, create_task
makes the intention clearer; ensure_future
is designed to accept a wider range of objects and procure a future of an unspecified type.
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