Skip to content

Commit 36846cc

Browse files
committed
NamedTask is subclassed from Cancellable. Doc changes.
1 parent 77f5a48 commit 36846cc

File tree

4 files changed

+171
-171
lines changed

4 files changed

+171
-171
lines changed

PRIMITIVES.md

Lines changed: 40 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -325,8 +325,9 @@ is next scheduled. This mechanism works with nested coros. However there is a
325325
limitation. If a coro issues `await uasyncio.sleep(secs)` or
326326
`uasyncio.sleep_ms(ms)` scheduling will not occur until the time has elapsed.
327327
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.
328+
There are other potential sources of latency in the form of slow code.
329+
`uasyncio` has no mechanism for verifying when cancellation has actually
330+
occurred. This library provides solutions.
330331

331332
Cancellation is supported by two classes, `Cancellable` and `NamedTask`. The
332333
`Cancellable` class allows the creation of named groups of anonymous tasks
@@ -347,7 +348,7 @@ latency.
347348

348349
The asynchronous `sleep` function takes two args:
349350
* `t` Mandatory. Time in seconds. May be integer or float.
350-
* `granularity` Optional. Integer >= 0, units ms. Default 100ms. Defines the
351+
* `granularity` Optional integer >= 0, units ms. Default 100ms. Defines the
351352
maximum latency. Small values reduce latency at cost of increased scheduler
352353
workload.
353354

@@ -374,12 +375,12 @@ async def comms(): # Perform some communications task
374375

375376
A `Cancellable` task is declared with the `@cancellable` decorator. When
376377
scheduled it will receive an initial arg which is a `TaskId` instance followed
377-
by any user-defined args. The `TaskId` instance can be ignored unless custom
378-
cleanup is required (see below).
378+
by any user-defined args. The `TaskId` instance is normally ignored however it
379+
can be useful for debugging - task_id() produces a unique integer task ID.
379380

380381
```python
381382
@cancellable
382-
async def print_nums(task_id, num):
383+
async def print_nums(_, num): # Discard task_id
383384
while True:
384385
print(num)
385386
num += 1
@@ -410,9 +411,8 @@ Constructor optional positional args:
410411
Constructor optional keyword arg:
411412
* `group` Integer or string. Default 0. See Groups below.
412413

413-
Class public methods:
414-
* `cancel_all` Asynchronous. In practice this is the only method required by
415-
user code.
414+
Public class method:
415+
* `cancel_all` Asynchronous.
416416
Optional args `group` default 0, `nowait` default `False`.
417417
The `nowait` arg is for use by the `NamedTask` derived class. The default
418418
value is assumed below.
@@ -424,14 +424,10 @@ Class public methods:
424424
the coro is written using the `@cancellable` decorator this is handled
425425
automatically.
426426
It is possible to trap the `StopTask` exception: see 'Custom cleanup' below.
427-
* `end` Synchronous. Arg: The coro task number. Informs the class that a
428-
`Cancellable` instance has ended, either normally or by cancellation.
429-
* `stopped` Synchronous. Arg: The coro task number. Informs the class that a
430-
Cancellable instance has been cancelled.
431427

432-
Bound method:
428+
Public bound method:
433429
* `__call__` This returns the coro and is used to schedule the task using the
434-
event loop `create_task()` method.
430+
event loop `create_task()` method using function call syntax.
435431

436432
### 4.2.1 Groups
437433

@@ -449,30 +445,27 @@ exception to perform custom cleanup operations. This may be done as below:
449445
```python
450446
@cancellable
451447
async def foo(task_id, arg):
452-
try:
453-
await sleep(1) # Main body of task
454-
except StopTask:
455-
# perform custom cleanup
456-
raise # Propagate exception to closure
448+
while True:
449+
try:
450+
await sleep(1) # Main body of task
451+
except StopTask:
452+
# perform custom cleanup
453+
return # Respond by quitting
457454
```
458455

459-
Where full control is required a cancellable task should be written without the
460-
decorator. The following example returns `True` if it ends normally or `False`
461-
if cancelled.
456+
The following example returns `True` if it ends normally or `False` if
457+
cancelled.
462458

463459
```python
460+
@cancellable
464461
async def bar(task_id):
465462
task_no = task_id() # Retrieve task no. from TaskId instance
466463
try:
467-
await sleep(1)
464+
await sleep(1) # Main body of task
468465
except StopTask:
469-
Cancellable.stopped(task_no)
470466
return False
471467
else:
472-
Cancellable.stopped(task_no)
473468
return True
474-
finally:
475-
Cancellable.end(task_no)
476469
```
477470

478471
###### [Contents](./PRIMITIVES.md#contents)
@@ -481,16 +474,16 @@ async def bar(task_id):
481474

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

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.
480+
A `NamedTask` coro is defined with the `@cancellable` decorator. When scheduled
481+
it will receive an initial arg which is a `TaskId` instance followed by any
482+
user-defined args. Normally the `task_id` is ignored as per `Cancellable`.
490483

491484
```python
492485
@cancellable
493-
async def foo(task_id, arg1, arg2):
486+
async def foo(_, arg1, arg2):
494487
await asyn.sleep(1)
495488
print('Task foo has ended.', arg1, arg2)
496489
```
@@ -524,29 +517,25 @@ Mandatory args:
524517
* Any further positional args are passed to the coro.
525518

526519
Optional keyword only arg:
527-
* `barrier` A `Barrier` instance may be passed if the cancelling task needs to
528-
wait for confirmation of successful cancellation.
520+
* `barrier` A `Barrier` instance may be passed. See below.
529521

530-
Class methods:
522+
Public class methods:
531523
* `cancel` Asynchronous. **[API change: was synchronous]**
532524
Mandatory arg: a coro name.
533525
Optional boolean arg `nowait` default `True`
534526
By default it will return soon. If `nowait` is `False` it will pause until the
535527
coro has completed cancellation.
536-
The named coro will receive a `CancelError` exception the next time it is
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.
528+
The named coro will receive a `StopTask` exception the next time it is
529+
scheduled. If the `@namedtask` decorator is used this is transparent to the
530+
user but the exception may be trapped for custom cleanup (see below).
540531
`cancel` will return `True` if the coro was cancelled. It will return `False`
541532
if the coro has already ended or been cancelled.
542533
* `is_running` Synchronous. Arg: A coro name. Returns `True` if coro is queued
543534
for scheduling, `False` if it has ended or been cancelled.
544-
* `end` Synchronous. Arg: A coro name. Run by the `NamedTask` instance to
545-
inform the class that the instance has ended. Completes quickly.
546535

547-
Bound method:
536+
Public bound method:
548537
* `__call__` This returns the coro and is used to schedule the task using the
549-
event loop `create_task()` method.
538+
event loop `create_task()` method using function call syntax.
550539

551540
### 4.3.1 Latency and Barrier objects
552541

@@ -570,39 +559,30 @@ See examples in `cantest.py` e.g. `cancel_test2()`.
570559

571560
A coroutine to be used as a `NamedTask` can intercept the `StopTask` exception
572561
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:
562+
The coro should have the following form:
575563

576564
```python
565+
@cancellable
577566
async def foo(task_id):
578567
try:
579568
await asyncio.sleep(1) # User code here
580-
NamedTask.stopped(task_id) # Inform class that it has stopped
581-
return True
582569
except StopTask:
583-
return False
584-
finally:
585-
# Inform class that it has stopped or been cancelled
586-
NamedTask.end(task_id)
570+
return False # Cleanup code
571+
else:
572+
return True # Normal exit
587573
```
588574

589575
### 4.3.3 Changes
590576

591577
The `NamedTask` class has been rewritten as a subclass of `Cancellable`. This
592578
is to simplify the code and to ensure accuracy of the `is_running` method. The
593579
latest API changes are:
594-
* `Cancellable.stopped()` is now synchronous.
580+
* `Cancellable.stopped()` is no longer a public method.
595581
* `NamedTask.cancel()` is now asynchronous.
596582
* `NamedTask` coros now receive a `TaskId` instance as their 1st arg.
597-
* The `@namedtask` works but is now an alias for `@cancellable`.
583+
* `@namedtask` still works but is now an alias for `@cancellable`.
598584

599585
The drive to simplify code comes from the fact that `uasyncio` is itself under
600586
development. Tracking changes is an inevitable headache.
601587

602588
###### [Contents](./PRIMITIVES.md#contents)
603-
604-
#### ExitGate (obsolete)
605-
606-
This was a nasty hack to fake task cancellation at a time when uasyncio did not
607-
support it. The code remains in the module to avoid breaking existing
608-
applications but it will be removed.

TUTORIAL.md

Lines changed: 52 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
# Application of uasyncio to hardware interfaces
22

3-
This document is a "work in progress" as I learn the content myself. Further
4-
at the time of writing uasyncio is itself under development. It is likely that
5-
these notes may contain errors; please report any you discover.
3+
This document is a "work in progress" as uasyncio is itself under development.
4+
Please report any errors you discover.
65

76
The MicroPython uasyncio library comprises a subset of Python's asyncio library
87
designed for use on microcontrollers. As such it has a small RAM footprint and
@@ -14,6 +13,11 @@ for a response from the hardware or for a user interaction.
1413
Another major application area for asyncio is in network programming: many
1514
guides to this may be found online.
1615

16+
Note that MicroPython is based on Python 3.4 with minimal Python 3.5 additions.
17+
Except where detailed below, `asyncio` features of versions >3.4 are
18+
unsupported. As stated above it is a subset. This document identifies supported
19+
features.
20+
1721
# Installing uasyncio on bare metal
1822

1923
The simplest approach is to use the upip utility. This involves installing the
@@ -84,7 +88,9 @@ examples below.
8488

8589
4.4 [Coroutines with timeouts](./TUTORIAL.md#44-coroutines-with-timeouts)
8690

87-
5. [Device driver examples](./TUTORIAL.md#5-device-driver-examples)
91+
4.5 [Exceptions](./TUTORIAL.md#45-exceptions)
92+
93+
5. [Device driver examples](./TUTORIAL.md#5-device-driver-examples)
8894

8995
5.1 [The IORead mechnaism](./TUTORIAL.md#51-the-ioread-mechanism)
9096

@@ -567,16 +573,40 @@ controlled. Documentation of this is in the code.
567573

568574
## 3.6 Task cancellation
569575

570-
This requires uasyncio.core V1.7 which was released on 16th Dec 2017, with
571-
firmware of that date or later. The following support classes are provided in
572-
`asyn.py`.
576+
This requires `uasyncio` V1.7.1 which was released on 7th Jan 2018, with
577+
firmware of that date or later.
578+
579+
`uasyncio` now provides a `cancel(coro)` function. This works by throwing an
580+
exception to the coro in a special way: cancellation is deferred until the coro
581+
is next scheduled. This mechanism works with nested coros. However there is a
582+
limitation. If a coro issues `await uasyncio.sleep(secs)` or
583+
`uasyncio.sleep_ms(ms)` scheduling will not occur until the time has elapsed.
584+
This introduces latency into cancellation which matters in some use-cases.
585+
Other potential sources of latency take the form of slow code. `uasyncio` has
586+
no mechanism for verifying when cancellation has actually occurred. The `asyn`
587+
library provides solutions via the following classes:
573588

574589
1. `Cancellable` This allows one or more tasks to be assigned to a group. A
575590
coro can cancel all tasks in the group, pausing until this has been acheived.
576591
Documentation may be found [here](./PRIMITIVES.md#42-class-cancellable).
577592
2. `NamedTask` This enables a coro to be associated with a user-defined name.
578-
The running status of named coros may be checked and they may individually be
579-
cancelled. Documentation may be found [here](./PRIMITIVES.md#43-class-namedtask).
593+
The running status of named coros may be checked. For advanced usage more
594+
complex groupings of tasks can be created. Documentation may be found
595+
[here](./PRIMITIVES.md#43-class-namedtask).
596+
597+
A typical use-case is as follows:
598+
599+
```python
600+
async def comms(): # Perform some communications task
601+
while True:
602+
await initialise_link()
603+
try:
604+
await do_communications() # Launches Cancellable tasks
605+
except CommsError:
606+
await Cancellable.cancel_all()
607+
# All sub-tasks are now known to be stopped. They can be re-started
608+
# with known initial state on next pass.
609+
```
580610

581611
Examples of the usage of these classes may be found in `asyn_demos.py`. For an
582612
illustration of the mechanism a cancellable task is defined as below:
@@ -772,6 +802,18 @@ response to the `TimeoutError` will correspondingly be delayed.
772802
If this matters to the application, create a long delay by awaiting a short one
773803
in a loop. The coro `asyn.sleep` [supports this](./PRIMITIVES.md#41-coro-sleep).
774804

805+
## 4.5 Exceptions
806+
807+
Where an exception occurs in a coro, it should be trapped either in that coro
808+
or in a coro which is awaiting its completion. This ensures that the exception
809+
is not propagated to the scheduler. If this occurred it would stop running,
810+
passing the exception to the code which started the scheduler.
811+
812+
Using `throw` to throw an exception to a coro is unwise. It subverts the design
813+
of `uasyncio` by forcing the coro to run, and possibly terminate, when it is
814+
still queued for execution. I haven't entirely thought through the implications
815+
of this, but it's a thoroughly bad idea.
816+
775817
###### [Contents](./TUTORIAL.md#contents)
776818

777819
# 5 Device driver examples
@@ -971,10 +1013,7 @@ In MicroPython coroutines are generators. This is not the case in CPython.
9711013
Issuing `yield` in a coro will provoke a syntax error in CPython, whereas in
9721014
MicroPython it has the same effect as `await asyncio.sleep(0)`. The surest way
9731015
to write error free code is to use CPython conventions and assume that coros
974-
are not generators. In some cases using the knowledge that coros are generators
975-
can be dangerous. For example using `throw` to throw an exception to a coro is
976-
a bad idea because it forces the coro to run, and possibly terminate, when it
977-
is still queued for execution.
1016+
are not generators.
9781017

9791018
The following will work. If you use them, be prepared to test your code against
9801019
each uasyncio release because the behaviour is not necessarily guaranteed.
@@ -988,12 +1027,6 @@ yield 100 # Pause 100ms - equivalent to above
9881027
Issuing `yield` or `yield 100` is slightly faster than the equivalent `await`
9891028
statements.
9901029

991-
**Exceptions**
992-
993-
Where an exception occurs in a coro, it should be trapped either in that coro
994-
or in a paused coro further up the calling chain. This ensures that the
995-
exception is not propagated to the scheduler, ensuring it continues to run.
996-
9971030
###### [Contents](./TUTORIAL.md#contents)
9981031

9991032
## 6.1 Program hangs

0 commit comments

Comments
 (0)