Skip to content

Commit 77f5a48

Browse files
committed
NamedTask subclassed from Cancellable. Part complete.
1 parent e64518f commit 77f5a48

File tree

4 files changed

+153
-123
lines changed

4 files changed

+153
-123
lines changed

PRIMITIVES.md

Lines changed: 77 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ MicroPython firmware or one built from source.
5151

5252
4.3.2 [Custom cleanup](./PRIMITIVES.md#432-custom-cleanup)
5353

54+
4.3.3 [Changes](./PRIMITIVES.md#433-changes)
55+
5456
## 1.1 Synchronisation Primitives
5557

5658
There is often a need to provide synchronisation between coros. A common
@@ -314,28 +316,29 @@ is raised.
314316

315317
# 4. Task Cancellation
316318

317-
Note to users of releases prior to 31st Dec 2017: this API has changed. It
318-
should now be stable.
319+
This has been under active development. Existing users please see
320+
[Changes](./PRIMITIVES.md#433-changes) for recent API changes.
319321

320-
In `uasyncio` task cancellation is achieved by throwing an exception to the
321-
coro to be cancelled in a special way: cancellation is deferred until the coro
322+
`uasyncio` now provides a `cancel(coro)` function. This works by throwing an
323+
exception to the coro in a special way: cancellation is deferred until the coro
322324
is next scheduled. This mechanism works with nested coros. However there is a
323325
limitation. If a coro issues `await uasyncio.sleep(secs)` or
324326
`uasyncio.sleep_ms(ms)` scheduling will not occur until the time has elapsed.
325327
This introduces latency into cancellation which matters in some use-cases.
328+
Crucially there is no inbuilt mechanism for verifying when cancellation has
329+
actually occurred. This library provides solutions.
326330

327331
Cancellation is supported by two classes, `Cancellable` and `NamedTask`. The
328332
`Cancellable` class allows the creation of named groups of anonymous tasks
329-
which may be cancelled as a group. Crucially this awaits actual completion of
330-
cancellation of all tasks in the group.
333+
which may be cancelled as a group. This awaits completion of cancellation of
334+
all tasks in the group.
331335

332336
The `NamedTask` class enables a task to be associated with a user supplied
333-
name, enabling it to be cancelled and its status checked. Cancellation does not
334-
await confirmation of completion. This may be achieved by means of a `Barrier`
335-
instance although the normal approach is to use a `Cancellable` task.
337+
name, enabling it to be cancelled and its status checked. Cancellation
338+
optionally awaits confirmation of completion.
336339

337340
For cases where cancellation latency is of concern `asyn.py` offers a `sleep`
338-
function which can reduce this.
341+
function which provides a delay which reduces latency.
339342

340343
## 4.1 Coro sleep
341344

@@ -404,22 +407,26 @@ Constructor mandatory args:
404407
Constructor optional positional args:
405408
* Any further positional args are passed to the coro.
406409

407-
Constructor optional keyword arg:
410+
Constructor optional keyword arg:
408411
* `group` Integer or string. Default 0. See Groups below.
409412

410-
Class methods:
411-
* `cancel_all` Asynchronous. Optional arg `group` default 0.
412-
Cancel all instances in the specified group and await completion. See Groups
413-
below.
413+
Class public methods:
414+
* `cancel_all` Asynchronous. In practice this is the only method required by
415+
user code.
416+
Optional args `group` default 0, `nowait` default `False`.
417+
The `nowait` arg is for use by the `NamedTask` derived class. The default
418+
value is assumed below.
419+
The method cancels all instances in the specified group and awaits completion.
420+
See Groups below.
414421
The `cancel_all` method will complete when all `Cancellable` instances have
415422
been cancelled or terminated naturally before `cancel_all` was launched.
416-
Each coro will receive a `CancelError` exception when it is next scheduled.
417-
The coro should trap this, await the `stopped` method and quit. If the coro
418-
quits for any reason it should call the `end` method. The `@cancellable`
419-
decorator handles the above housekeeping.
423+
Each coro will receive a `StopTask` exception when it is next scheduled. If
424+
the coro is written using the `@cancellable` decorator this is handled
425+
automatically.
426+
It is possible to trap the `StopTask` exception: see 'Custom cleanup' below.
420427
* `end` Synchronous. Arg: The coro task number. Informs the class that a
421428
`Cancellable` instance has ended, either normally or by cancellation.
422-
* `stopped` Asynchronous. Arg: The coro task number. Informs the class that a
429+
* `stopped` Synchronous. Arg: The coro task number. Informs the class that a
423430
Cancellable instance has been cancelled.
424431

425432
Bound method:
@@ -459,9 +466,10 @@ async def bar(task_id):
459466
try:
460467
await sleep(1)
461468
except StopTask:
462-
await Cancellable.stopped(task_no)
469+
Cancellable.stopped(task_no)
463470
return False
464471
else:
472+
Cancellable.stopped(task_no)
465473
return True
466474
finally:
467475
Cancellable.end(task_no)
@@ -473,14 +481,16 @@ async def bar(task_id):
473481

474482
A `NamedTask` instance is associated with a user-defined name such that the
475483
name may outlive the task: a coro may end but the class enables its state to be
476-
checked.
484+
checked. It is a subclass of `Cancellable` and ts constructor disallows
485+
duplicate names: each instance of a coro must be assigned a unique name.
477486

478-
A `NamedTask` task is defined with the `@namedtask` decorator. When scheduled it
479-
will receive an initial arg which is the name followed by any user-defined args.
487+
A `NamedTask` coro is normally defined with the `@cancellable` decorator. When
488+
scheduled it will receive an initial arg which is a `TaskId` instance followed
489+
by any user-defined args. Normally the `task_id` can be ignored.
480490

481491
```python
482-
@namedtask
483-
async def foo(name, arg1, arg2):
492+
@cancellable
493+
async def foo(task_id, arg1, arg2):
484494
await asyn.sleep(1)
485495
print('Task foo has ended.', arg1, arg2)
486496
```
@@ -494,10 +504,10 @@ loop = asyncio.get_event_loop() # Or schedule and continue:
494504
loop.create_task(NamedTask('my nums', foo, 10, 11)()) # Note () syntax.
495505
```
496506

497-
Cancellation is performed with
507+
Cancellation is performed with:
498508

499509
```python
500-
NamedTask.cancel('my foo')
510+
await NamedTask.cancel('my foo') # API change
501511
```
502512

503513
When cancelling a task there is no need to check if the task is still running:
@@ -518,17 +528,20 @@ Mandatory args:
518528
wait for confirmation of successful cancellation.
519529

520530
Class methods:
521-
* `cancel` Synchronous. Arg: a coro name.
531+
* `cancel` Asynchronous. **[API change: was synchronous]**
532+
Mandatory arg: a coro name.
533+
Optional boolean arg `nowait` default `True`
534+
By default it will return soon. If `nowait` is `False` it will pause until the
535+
coro has completed cancellation.
522536
The named coro will receive a `CancelError` exception the next time it is
523-
scheduled. The coro should trap this, ensure the `end` bound coro is launched
524-
and return. The `@namedtask` decorator handles this, ensuring `end` is called
525-
under all circumstances.
537+
scheduled. The coro should trap this, call the `end` method and return. The
538+
`@namedtask` decorator handles this, ensuring `end` is called under all
539+
circumstances.
526540
`cancel` will return `True` if the coro was cancelled. It will return `False`
527541
if the coro has already ended or been cancelled.
528542
* `is_running` Synchronous. Arg: A coro name. Returns `True` if coro is queued
529-
for scheduling, `False` if it has ended or been scheduled for cancellation.
530-
See note in 4.3.1 below.
531-
* `end` Asynchronous. Arg: A coro name. Run by the `NamedTask` instance to
543+
for scheduling, `False` if it has ended or been cancelled.
544+
* `end` Synchronous. Arg: A coro name. Run by the `NamedTask` instance to
532545
inform the class that the instance has ended. Completes quickly.
533546

534547
Bound method:
@@ -537,43 +550,55 @@ Bound method:
537550

538551
### 4.3.1 Latency and Barrier objects
539552

540-
Consider the latency discussed at the start of section 3.6: cancellation raises
541-
an exception which will be handled when the coro is next scheduled. There is no
542-
mechanism to determine if a cancelled task has been scheduled and has acted on
543-
the `StopTask` exception. Consequently calling `is_running()` on a recently
544-
cancelled task may return `False` even though `uasyncio` will run the task for
545-
one final time.
546-
547-
Confirmation of cancellation may be achieved by means of a `Barrier` object,
548-
however practical use-cases for this are few - if confirmation is required the
549-
normal approach is to use `Cancellable` tasks, if necessary in groups having a
550-
single member. However the approach is described below.
553+
It is possible to get confirmation of cancellation of an arbitrary set of
554+
`NamedTask` instances by instantiating a `Barrier` and passing it to the
555+
constructor of each member. Practical use-cases for this are few - the normal
556+
approach is to use a group of `Cancellable` tasks. The approach is described
557+
below.
551558

552559
If a `Barrier` instance is passed to the `NamedTask` constructor, a task
553560
performing cancellation can pause until a set of cancelled tasks have
554561
terminated. The `Barrier` is constructed with the number of dependent tasks
555562
plus one (the task which is to wait on it). It is passed to the constructor of
556563
each dependent task and the cancelling task waits on it after cancelling all
557-
dependent tasks. Note that the tasks being cancelled terminate immediately.
564+
dependent tasks. Each task being cancelled terminates 'immediately' subject
565+
to latency.
558566

559567
See examples in `cantest.py` e.g. `cancel_test2()`.
560568

561569
### 4.3.2 Custom cleanup
562570

563-
A task created with the `@namedtask` decorator can intercept the `StopTask`
564-
exception if necessary. This might be done for cleanup or to return a
565-
'cancelled' status.
571+
A coroutine to be used as a `NamedTask` can intercept the `StopTask` exception
572+
if necessary. This might be done for cleanup or to return a 'cancelled' status.
573+
To do this, do not use the `@cancellable` decorator. The coro should have the
574+
following form:
566575

567576
```python
568-
@namedtask
569-
async def foo(_):
577+
async def foo(task_id):
570578
try:
571579
await asyncio.sleep(1) # User code here
580+
NamedTask.stopped(task_id) # Inform class that it has stopped
572581
return True
573582
except StopTask:
574583
return False
584+
finally:
585+
# Inform class that it has stopped or been cancelled
586+
NamedTask.end(task_id)
575587
```
576588

589+
### 4.3.3 Changes
590+
591+
The `NamedTask` class has been rewritten as a subclass of `Cancellable`. This
592+
is to simplify the code and to ensure accuracy of the `is_running` method. The
593+
latest API changes are:
594+
* `Cancellable.stopped()` is now synchronous.
595+
* `NamedTask.cancel()` is now asynchronous.
596+
* `NamedTask` coros now receive a `TaskId` instance as their 1st arg.
597+
* The `@namedtask` works but is now an alias for `@cancellable`.
598+
599+
The drive to simplify code comes from the fact that `uasyncio` is itself under
600+
development. Tracking changes is an inevitable headache.
601+
577602
###### [Contents](./PRIMITIVES.md#contents)
578603

579604
#### ExitGate (obsolete)

0 commit comments

Comments
 (0)