Skip to content

Commit 97653a3

Browse files
committed
asyn.py Condition and Gather classes added.
1 parent 59a9207 commit 97653a3

File tree

5 files changed

+460
-43
lines changed

5 files changed

+460
-43
lines changed

PRIMITIVES.md

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,15 @@ MicroPython firmware or one built from source.
3434
3.5 [Class Semaphore](./PRIMITIVES.md#35-class-semaphore)
3535

3636
3.5.1 [Class BoundedSemaphore](./PRIMITIVES.md#351-class-boundedsemaphore)
37-
37+
38+
3.6 [Class Condition](./PRIMITIVES.md#36-class-condition)
39+
40+
3.6.1 [Definition](./PRIMITIVES.md#361-definition)
41+
42+
3.7 [Class Gather](./PRIMITIVES.md#37-class-gather)
43+
44+
3.7.1 [Definition](./PRIMITIVES.md#371-definition)
45+
3846
4. [Task Cancellation](./PRIMITIVES.md#4-task-cancellation)
3947

4048
4.1 [Coro sleep](./PRIMITIVES.md#41-coro-sleep)
@@ -302,6 +310,104 @@ is raised.
302310

303311
###### [Contents](./PRIMITIVES.md#contents)
304312

313+
## 3.6 Class Condition
314+
315+
A `Condition` instance enables controlled access to a shared resource. In
316+
typical applications a number of tasks wait for the resource to be available.
317+
Once this occurs access can be controlled both by the number of tasks and by
318+
means of a `Lock`.
319+
320+
A task waiting on a `Condition` instance will pause until another task issues
321+
`condition.notify(n)` or `condition.notify_all()`. If the number of tasks
322+
waiting on the condition exceeds `n`, only `n` tasks will resume. A `Condition`
323+
instance has a `Lock` as a member. A task will only resume when it has acquired
324+
the lock. User code may release the lock as required by the application logic.
325+
326+
Typical use of the class is in a synchronous context manager:
327+
328+
```python
329+
with await cond:
330+
cond.notify(2) # Notify 2 tasks
331+
```
332+
333+
```python
334+
with await cond:
335+
await cond.wait()
336+
# Has been notified and has access to the locked resource
337+
# Resource has been unocked by context manager
338+
```
339+
### 3.6.1 Definition
340+
341+
Constructor: Optional arg `lock=None`. A `Lock` instance may be specified,
342+
otherwise the `Condition` instantiates its own.
343+
344+
Synchronous methods:
345+
* `locked` No args. Returns the state of the `Lock` instance.
346+
* `release` No args. Release the `Lock`. A `RuntimeError` will occur if the
347+
`Lock` is not locked.
348+
* `notify` Arg `n=1`. Notify `n` tasks. The `Lock` must be acquired before
349+
issuing `notify` otherwise a `RuntimeError` will occur.
350+
* `notify_all` No args. Notify all tasks. The `Lock` must be acquired before
351+
issuing `notify_all` otherwise a `RuntimeError` will occur.
352+
353+
Asynchronous methods:
354+
* `acquire` No args. Pause until the `Lock` is acquired.
355+
* `wait` No args. Await notification and the `Lock`. The `Lock` must be
356+
acquired before issuing `wait` otherwise a `RuntimeError` will occur. The
357+
sequence is as follows:
358+
The `Lock` is released.
359+
The task pauses until another task issues `notify`.
360+
It continues to pause until the `Lock` has been re-acquired when execution
361+
resumes.
362+
* `wait_for` Arg: `predicate` a callback returning a `bool`. The task pauses
363+
until a notification is received and an immediate test of `predicate()`
364+
returns `True`.
365+
366+
###### [Contents](./PRIMITIVES.md#contents)
367+
368+
## 3.7 Class Gather
369+
370+
This aims to replicate some of the functionality of `asyncio.gather` in a
371+
'micro' form. The user creates a list of `Gatherable` tasks and then awaits a
372+
`Gather` object. When the last task to complete terminates, this will return a
373+
list of results returned by the tasks. Timeouts may be assigned to individual
374+
tasks.
375+
376+
```python
377+
async def bar(x, y, rats): # Example coro: note arg passing
378+
await asyncio.sleep(1)
379+
return x * y * rats
380+
381+
gatherables = [asyn.Gatherable(foo, n) for n in range(4)]
382+
gatherables.append(asyn.Gatherable(bar, 7, 8, rats=77))
383+
gatherables.append(asyn.Gatherable(rats, 0, timeout=5))
384+
res = await asyn.Gather(gatherables)
385+
```
386+
387+
The result `res` is a 6 element list containing the result of each of the 6
388+
coros. These are ordered by the position of the coro in the `gatherables` list.
389+
This is as per `asyncio.gather()`.
390+
391+
See `asyntest.py` function `gather_test()`.
392+
393+
### 3.7.1 Definition
394+
395+
The `Gatherable` class has no user methods. The constructor takes a coro by
396+
name followed by any positional or keyword arguments for the coro. If an arg
397+
`timeout` is provided it should have an integer or float value: this is taken
398+
to be the timeout for the coro in seconds.
399+
400+
The `Gather` class has no user methods. The constructor takes one mandatory
401+
arg: a list of `Gatherable` instances.
402+
403+
`Gather` instances are awaitable. An `await` on an instance will terminate when
404+
the last member task completes or times out. It returns a list whose length
405+
matches the length of the list of `Gatherable` instances. Each element contains
406+
the return value of the corresponding `Gatherable` instance. Each return value
407+
may be of any type.
408+
409+
###### [Contents](./PRIMITIVES.md#contents)
410+
305411
# 4. Task Cancellation
306412

307413
This has been under active development. Existing users please see

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,17 @@ cancellation.
6161

6262
Classes `Task` and `Future` are not supported.
6363

64+
## Synchronisation Primitives and Task Cancellation
65+
66+
The library `asyn.py` provides 'micro' implementations of the `asyncio`
67+
[synchronisation primitives](https://docs.python.org/3/library/asyncio-sync.html).
68+
Because `uasyncio` does not support `Task` and `Future` classes `asyncio`
69+
features such as `wait` and `gather` are unavailable. A `Barrier` class enables
70+
coroutines to be similarly synchronised. Coroutine cancellation is performed in
71+
a special efficient manner in `uasyncio`. The `asyn` library enhances this by
72+
facilitating options to pause until cancellation is complete and to check the
73+
status of individual coroutines.
74+
6475
## Asynchronous I/O
6576

6677
At the time of writing this was under development. Asynchronous I/O works with

TUTORIAL.md

Lines changed: 92 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -99,10 +99,14 @@ $ python3 -m micropip.py install -p ~/syn micropython-uasyncio
9999

100100
4.1 [Awaitable classes](./TUTORIAL.md#41-awaitable-classes)
101101

102+
4.1.1 [Use in context managers](./TUTORIAL.md#411-use-in-context-managers)
103+
104+
4.1.2 [Awaiting a coro](./TUTORIAL.md#412-awaiting-a-coro)
105+
102106
4.2 [Asynchronous iterators](./TUTORIAL.md#42-asynchronous-iterators)
103107

104108
4.3 [Asynchronous context managers](./TUTORIAL.md#43-asynchronous-context-managers)
105-
109+
106110
4.4 [Coroutines with timeouts](./TUTORIAL.md#44-coroutines-with-timeouts)
107111

108112
4.5 [Exceptions](./TUTORIAL.md#45-exceptions)
@@ -430,7 +434,8 @@ ineffective. It will not receive the `TimeoutError` until it has acquired the
430434
lock. The same observation applies to task cancellation.
431435

432436
The module `asyn.py` offers a `Lock` class which works in these situations. It
433-
is significantly less efficient than the official class.
437+
is significantly less efficient than the official class but supports additional
438+
interfaces as per the CPython version including context manager usage.
434439

435440
###### [Contents](./TUTORIAL.md#contents)
436441

@@ -495,11 +500,14 @@ compensation for this.
495500

496501
## 3.3 Barrier
497502

498-
This enables multiple coros to rendezvous at a particular point. For example
499-
producer and consumer coros can synchronise at a point where the producer has
500-
data available and the consumer is ready to use it. At that point in time the
501-
`Barrier` can optionally run a callback before releasing the barrier and
502-
allowing all waiting coros to continue. [Full details.](./PRIMITIVES.md#34-class-barrier)
503+
This has two uses. Firstly it can cause a coro to pause until one or more other
504+
coros have terminated.
505+
506+
Secondly it enables multiple coros to rendezvous at a particular point. For
507+
example producer and consumer coros can synchronise at a point where the
508+
producer has data available and the consumer is ready to use it. At that point
509+
in time the `Barrier` can run an optional callback before the barrier is
510+
released and all waiting coros can continue. [Full details.](./PRIMITIVES.md#34-class-barrier)
503511

504512
The callback can be a function or a coro. In most applications a function is
505513
likely to be used: this can be guaranteed to run to completion before the
@@ -674,24 +682,91 @@ class Foo():
674682
for n in range(5):
675683
print('__await__ called')
676684
yield from asyncio.sleep(1) # Other coros get scheduled here
685+
return 42
677686

678687
__iter__ = __await__ # See note below
679688

680689
async def bar():
681690
foo = Foo() # Foo is an awaitable class
682691
print('waiting for foo')
683-
await foo
684-
print('done')
692+
res = await foo # Retrieve result
693+
print('done', res)
685694

686695
loop = asyncio.get_event_loop()
687696
loop.run_until_complete(bar())
688697
```
689698

690699
Currently MicroPython doesn't support `__await__` (issue #2678) and
691700
`__iter__` must be used. The line `__iter__ = __await__` enables portability
692-
between CPython and MicroPython.
701+
between CPython and MicroPython. Example code may be found in the `Event`,
702+
`Barrier`, `Cancellable` and `Condition` classes in asyn.py.
703+
704+
### 4.1.1 Use in context managers
705+
706+
Awaitable objects can be used in synchronous or asynchronous CM's by providing
707+
the necessary special methods. The syntax is:
708+
709+
```python
710+
with await awaitable as a: # The 'as' clause is optional
711+
# code omitted
712+
async with awaitable as a: # Asynchronous CM (see below)
713+
# do something
714+
```
715+
716+
To achieve this the `__await__` generator should return `self`. This is passed
717+
to any variable in an `as` clause and also enables the special methods to work.
718+
See `asyn.Condition` and `asyntest.condition_test`, where the `Condition` class
719+
is awaitable and may be used in a synchronous CM.
720+
721+
###### [Contents](./TUTORIAL.md#contents)
722+
723+
### 4.1.2 Awaiting a coro
724+
725+
The Python language requires that `__await__` is a generator function. In
726+
MicroPython generators and coroutines are identical, so the solution is to use
727+
`yield from coro(args)`.
728+
729+
This tutorial aims to offer code portable to CPython 3.5 or above. In CPython
730+
coroutines and generators are distinct. CPython coros have an `__await__`
731+
special method which retrieves a generator. This is portable:
732+
733+
```python
734+
up = False # Running under MicroPython?
735+
try:
736+
import uasyncio as asyncio
737+
up = True # Or can use sys.implementation.name
738+
except ImportError:
739+
import asyncio
693740

694-
Example code may be found in the `Event` and `Barrier` classes in asyn.py.
741+
async def times_two(n): # Coro to await
742+
await asyncio.sleep(1)
743+
return 2 * n
744+
745+
class Foo():
746+
def __await__(self):
747+
res = 1
748+
for n in range(5):
749+
print('__await__ called')
750+
if up: # MicroPython
751+
res = yield from times_two(res)
752+
else: # CPython
753+
res = yield from times_two(res).__await__()
754+
return res
755+
756+
__iter__ = __await__
757+
758+
async def bar():
759+
foo = Foo() # foo is awaitable
760+
print('waiting for foo')
761+
res = await foo # Retrieve value
762+
print('done', res)
763+
764+
loop = asyncio.get_event_loop()
765+
loop.run_until_complete(bar())
766+
```
767+
768+
Note that, in `__await__`, `yield from asyncio.sleep(1)` is allowed by CPython.
769+
I haven't yet established how this is achieved.
695770

696771
###### [Contents](./TUTORIAL.md#contents)
697772

@@ -761,18 +836,22 @@ async def bar(lock):
761836
As with normal context managers an exit method is guaranteed to be called when
762837
the context manager terminates, whether normally or via an exception. To
763838
achieve this the special methods `__aenter__` and `__aexit__` must be
764-
defined, both being coros waiting on an `awaitable` object. This example comes
765-
from the `Lock` class:
839+
defined, both being coros waiting on a coro or `awaitable` object. This example
840+
comes from the `Lock` class:
766841

767842
```python
768843
async def __aenter__(self):
769844
await self.acquire() # a coro defined with async def
845+
return self
770846

771847
async def __aexit__(self, *args):
772848
self.release() # A conventional method
773849
await asyncio.sleep_ms(0)
774850
```
775851

852+
If the `async with` has an `as variable` clause the variable receives the
853+
value returned by `__aenter__`.
854+
776855
Note there is currently a bug in the implementation whereby if an explicit
777856
`return` is issued within an `async with` block, the `__aexit__` method
778857
is not called. The solution is to design the code so that in all cases it runs

0 commit comments

Comments
 (0)