Skip to content

Commit c3f859a

Browse files
committed
Tutorial: improve task cancellation section.
1 parent 190a0f9 commit c3f859a

File tree

1 file changed

+63
-15
lines changed

1 file changed

+63
-15
lines changed

TUTORIAL.md

Lines changed: 63 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -605,29 +605,77 @@ controlled. Documentation of this is in the code.
605605
## 3.6 Task cancellation
606606

607607
`uasyncio` provides a `cancel(coro)` function. This works by throwing an
608-
exception to the coro in a special way: cancellation is deferred until the coro
609-
is next scheduled. This mechanism works with nested coros. However there is a
610-
limitation. If a coro issues `await uasyncio.sleep(secs)` or
611-
`uasyncio.sleep_ms(ms)` scheduling will not occur until the time has elapsed.
612-
This introduces latency into cancellation which matters in some use-cases.
613-
Another source of latency is where a task is waiting on I/O. In many
614-
applications it is necessary for the task performing cancellation to pause
615-
until all cancelled coros have actually stopped.
616-
617-
If the task to be cancelled only pauses on zero delays and never waits on I/O,
618-
the round-robin nature of the scheduler avoids the need to verify cancellation:
608+
exception to the coro in a special way: when the coro is next scheduled it
609+
receives the exception. This mechanism works with nested coros. Usage is as
610+
follows:
611+
```python
612+
async def foo():
613+
while True:
614+
# do something every 10 secs
615+
await asyncio.sleep(10)
616+
617+
async def bar(loop):
618+
foo_instance = foo() # Create a coroutine instance
619+
loop.create_task(foo_instance)
620+
# code omitted
621+
asyncio.cancel(foo_instance)
622+
```
623+
In this example when `bar` issues `cancel` it will not take effect until `foo`
624+
is next scheduled. There is thus a latency of up to 10s in the cancellation of
625+
`foo`. Another source of latency would arise if `foo` waited on I/O. Where
626+
latency arises, `bar` cannot determine whether `foo` has yet been cancelled.
627+
This matters in some use-cases.
628+
629+
In many applications it is necessary for the task performing cancellation to
630+
pause until all cancelled coros have actually stopped. If the task to be
631+
cancelled only pauses on zero delays and never waits on I/O, the round-robin
632+
nature of the scheduler avoids the need to verify cancellation:
619633

620634
```python
621635
asyncio.cancel(my_coro)
622636
await asyncio.sleep(0) # Ensure my_coro gets scheduled with the exception
623637
# my_coro will be cancelled now
624638
```
625639
This does require that all coros awaited by `my_coro` also meet the zero delay
626-
criterion.
640+
criterion. For the general case where latency exists, solutions are discussed
641+
below.
642+
643+
Behaviour which may surprise the unwary arises when a coro to be cancelled is
644+
awaited rather than being launched by `create_task`. Consider this fragment:
645+
646+
```python
647+
async def foo():
648+
while True:
649+
# do something every 10 secs
650+
await asyncio.sleep(10)
651+
652+
async def foo_runner(foo_instance):
653+
await foo_instance
654+
print('This will not be printed')
655+
656+
async def bar(loop):
657+
foo_instance = foo()
658+
loop.create_task(foo_runner(foo_instance))
659+
# code omitted
660+
asyncio.cancel(foo_instance)
661+
```
662+
When `cancel` is called and `foo` is next scheduled it is removed from the
663+
scheduler's queue; because it lacks a `return` statement the calling routine
664+
`foo_runner` never resumes. The solution is to trap the exception:
665+
```python
666+
async def foo():
667+
try:
668+
while True:
669+
# do something every 10 secs
670+
await asyncio.sleep(10)
671+
except asyncio.CancelledError:
672+
return
673+
```
627674

628-
That special case notwithstanding, `uasyncio` lacks a mechanism for verifying
629-
when cancellation has actually occurred. The `asyn` library provides
630-
verification via the following classes:
675+
In general `uasyncio` lacks a mechanism for verifying when cancellation has
676+
actually occurred. Ad-hoc mechanisms based on trapping `CancelledError` may be
677+
devised. For convenience the `asyn` library provides means of awaiting the
678+
cancellation of one or more coros via these classes:
631679

632680
1. `Cancellable` This allows one or more tasks to be assigned to a group. A
633681
coro can cancel all tasks in the group, pausing until this has been achieved.

0 commit comments

Comments
 (0)