jsstuball
jsstuball

Reputation: 4911

Is it ever useful to cancel an asyncio future?

I am trying to understand the use of Future.cancel() with asyncio. The Python documentation is very light on this. I've had no success with existing questions on here or search engines. I just want to understand happens when a task is awaiting a future which is cancelled.

Here is my code:

import asyncio 

async def foo(future):
    await asyncio.sleep(3)
    future.cancel()

async def bar(future):
    await future
    print("hi")

async def baz(future):
    await bar(future)
    print("ho")

loop = asyncio.get_event_loop()
future = loop.create_future()
loop.create_task(baz(future))
loop.create_task(foo(future))
loop.run_forever()

"hi" is not seen printed. So I initially guessed bar was returning at the line await future in the case of a cancel.

However, "ho" is not printed either. So it seems logical that cancelling a future never yields back to tasks awaiting it? But then these tasks are sitting in the event loop forever? This seems undesirable, where have I misunderstood?

Upvotes: 2

Views: 4247

Answers (1)

user4815162342
user4815162342

Reputation: 154856

In this case the answer lies in the documentation, but you have to look for it a bit. First, a reminder of what it means to await a future:

# the expression:
x = await future

# is equivalent to:
... magically suspend the coroutine until the future.done() becomes true ...
x = future.result()

In other words, once the execution of the coroutine that contains await resumes, the value of the await statement will be the result() of the awaited future.

The question is: when you cancel a future, what is its result? The documentation says:

If the Future has been cancelled, this method raises a CancelledError exception.

So when someone cancel a future you awaited, the await future expression will raise an exception! This neatly explains why bar doesn't print hi (because await future has raised), and why baz doesn't print ho (because await bar(...) has raised).

A traceback is never printed because loop.create_task spawns the coroutine in the "background" (of sorts) - if no one inspects the return value, the exception will be lost. And since you threw away the task object returned by create_task and used run_forever to have the loop running forever, the loop just continues running, waiting (forever) for new tasks to somehow arrive.

If you changed the code to actually collect the result of bar, you would easily observe the CancelledError:

if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    future = loop.create_future()
    loop.create_task(foo(future))
    loop.run_until_complete(baz(future))

Output:

Traceback (most recent call last):
  File "xxx.py", line 19, in <module>
    loop.run_until_complete(baz(future))
  File "/usr/lib/python3.5/asyncio/base_events.py", line 387, in run_until_complete
    return future.result()
  File "/usr/lib/python3.5/asyncio/futures.py", line 266, in result
    raise CancelledError
concurrent.futures._base.CancelledError

Upvotes: 5

Related Questions