Skip to content

Commit c300516

Browse files
committed
Changes to primitives/Message to use ThreadSafeFlag.
1 parent 6296f21 commit c300516

File tree

2 files changed

+110
-75
lines changed

2 files changed

+110
-75
lines changed

v3/docs/TUTORIAL.md

Lines changed: 69 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,16 @@ invocation lines alone should be changed. e.g. :
549549
from uasyncio import Semaphore, BoundedSemaphore
550550
from uasyncio import Queue
551551
```
552+
##### Note on CPython compatibility
553+
554+
CPython will throw a `RuntimeError` on first use of a synchronisation primitive
555+
that was instantiated prior to starting the scheduler. By contrast
556+
`MicroPython` allows instantiation in synchronous code executed before the
557+
scheduler is started. Early instantiation can be advantageous in low resource
558+
environments. For example a class might have a large buffer and bound `Event`
559+
instances. Such a class should be instantiated early, before RAM fragmentation
560+
sets in.
561+
552562
The following provides a discussion of the primitives.
553563

554564
###### [Contents](./TUTORIAL.md#contents)
@@ -625,15 +635,15 @@ using it:
625635
import uasyncio as asyncio
626636
from uasyncio import Event
627637

628-
event = Event()
629-
async def waiter():
638+
async def waiter(event):
630639
print('Waiting for event')
631640
await event.wait() # Pause here until event is set
632641
print('Waiter got event.')
633642
event.clear() # Flag caller and enable re-use of the event
634643

635644
async def main():
636-
asyncio.create_task(waiter())
645+
event = Event()
646+
asyncio.create_task(waiter(event))
637647
await asyncio.sleep(2)
638648
print('Setting event')
639649
event.set()
@@ -915,6 +925,19 @@ tim = Timer(1, freq=1, callback=cb)
915925

916926
asyncio.run(foo())
917927
```
928+
Another example (posted by [Damien](https://github.com/micropython/micropython/pull/6886#issuecomment-779863757)):
929+
```python
930+
class AsyncPin:
931+
def __init__(self, pin, trigger):
932+
self.pin = pin
933+
self.flag = ThreadSafeFlag()
934+
self.pin.irq(lambda pin: self.flag.set(), trigger, hard=True)
935+
936+
def wait_edge(self):
937+
return self.flag.wait()
938+
```
939+
You then call `await async_pin.wait_edge()`.
940+
918941
The current implementation provides no performance benefits against polling the
919942
hardware. The `ThreadSafeFlag` uses the I/O mechanism. There are plans to
920943
reduce the latency such that I/O is polled every time the scheduler acquires
@@ -1106,25 +1129,22 @@ finally:
11061129

11071130
## 3.9 Message
11081131

1109-
This is an unofficial primitive with no counterpart in CPython asyncio. It has
1110-
largely been superseded by [ThreadSafeFlag](./TUTORIAL.md#36-threadsafeflag).
1132+
This is an unofficial primitive with no counterpart in CPython asyncio. It uses
1133+
[ThreadSafeFlag](./TUTORIAL.md#36-threadsafeflag) to provide an object similar
1134+
to `Event` but capable of being set in a hard ISR context. It extends
1135+
`ThreadSafeFlag` so that multiple tasks can wait on an ISR.
11111136

1112-
This is similar to the `Event` class. It differs in that:
1137+
It is similar to the `Event` class. It differs in that:
11131138
* `.set()` has an optional data payload.
11141139
* `.set()` is capable of being called from a hard or soft interrupt service
11151140
routine.
11161141
* It is an awaitable class.
1117-
1118-
Limitation: `Message` is intended for 1:1 operation where a single task waits
1119-
on a message from another task or ISR. The receiving task should issue
1120-
`.clear`.
1142+
* The logic of `.clear` differs: it must be called by at least one task which
1143+
waits on the `Message`.
11211144

11221145
The `.set()` method can accept an optional data value of any type. The task
1123-
waiting on the `Message` can retrieve it by means of `.value()`. Note that
1124-
`.clear()` will set the value to `None`. One use for this is for the task
1125-
setting the `Message` to issue `.set(utime.ticks_ms())`. The task waiting on
1126-
the `Message` can determine the latency incurred, for example to perform
1127-
compensation for this.
1146+
waiting on the `Message` can retrieve it by means of `.value()` or by awaiting
1147+
the `Message` as below.
11281148

11291149
Like `Event`, `Message` provides a way a task to pause until another flags it
11301150
to continue. A `Message` object is instantiated and made accessible to the task
@@ -1136,8 +1156,7 @@ from primitives.message import Message
11361156

11371157
async def waiter(msg):
11381158
print('Waiting for message')
1139-
await msg
1140-
res = msg.value()
1159+
res = await msg
11411160
print('waiter got', res)
11421161
msg.clear()
11431162

@@ -1155,15 +1174,44 @@ and a task. The handler services the hardware and issues `.set()` which is
11551174
tested in slow time by the task.
11561175

11571176
Constructor:
1158-
* Optional arg `delay_ms=0` Polling interval.
1177+
* No args.
11591178
Synchronous methods:
11601179
* `set(data=None)` Trigger the message with optional payload.
1161-
* `is_set()` Return `True` if the message is set.
1162-
* `clear()` Clears the triggered status and sets payload to `None`.
1180+
* `is_set()` Returns `True` if the `Message` is set, `False` if `.clear()` has
1181+
beein issued.
1182+
* `clear()` Clears the triggered status. At least one task waiting on the
1183+
message should issue `clear()`.
11631184
* `value()` Return the payload.
11641185
Asynchronous Method:
11651186
* `wait` Pause until message is triggered. You can also `await` the message as
1166-
per the above example.
1187+
per the examples.
1188+
1189+
The following example shows multiple tasks awaiting a `Message`.
1190+
```python
1191+
from primitives.message import Message
1192+
import uasyncio as asyncio
1193+
1194+
async def bar(msg, n):
1195+
while True:
1196+
res = await msg
1197+
msg.clear()
1198+
print(n, res)
1199+
# Pause until other coros waiting on msg have run and before again
1200+
# awaiting a message.
1201+
await asyncio.sleep_ms(0)
1202+
1203+
async def main():
1204+
msg = Message()
1205+
for n in range(5):
1206+
asyncio.create_task(bar(msg, n))
1207+
k = 0
1208+
while True:
1209+
k += 1
1210+
await asyncio.sleep_ms(1000)
1211+
msg.set('Hello {}'.format(k))
1212+
1213+
asyncio.run(main())
1214+
```
11671215

11681216
## 3.10 Synchronising to hardware
11691217

v3/primitives/message.py

Lines changed: 41 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,57 @@
11
# message.py
2+
# Now uses ThreadSafeFlag for efficiency
23

3-
# Copyright (c) 2018-2020 Peter Hinch
4+
# Copyright (c) 2018-2021 Peter Hinch
45
# Released under the MIT License (MIT) - see LICENSE file
56

7+
# Usage:
8+
# from primitives.message import Message
9+
610
try:
711
import uasyncio as asyncio
812
except ImportError:
913
import asyncio
10-
# Usage:
11-
# from primitives.message import Message
1214

1315
# A coro waiting on a message issues await message
14-
# A coro rasing the message issues message.set(payload)
15-
# When all waiting coros have run
16-
# message.clear() should be issued
17-
18-
# This more efficient version is commented out because Event.set is not ISR
19-
# friendly. TODO If it gets fixed, reinstate this (tested) version and update
20-
# tutorial for 1:n operation.
21-
#class Message(asyncio.Event):
22-
#def __init__(self, _=0):
23-
#self._data = None
24-
#super().__init__()
25-
26-
#def clear(self):
27-
#self._data = None
28-
#super().clear()
29-
30-
#def __await__(self):
31-
#await super().wait()
32-
33-
#__iter__ = __await__
34-
35-
#def set(self, data=None):
36-
#self._data = data
37-
#super().set()
38-
39-
#def value(self):
40-
#return self._data
41-
42-
# Has an ISR-friendly .set()
43-
class Message():
44-
def __init__(self, delay_ms=0):
45-
self.delay_ms = delay_ms
46-
self.clear()
47-
48-
def clear(self):
49-
self._flag = False
50-
self._data = None
51-
52-
async def wait(self): # CPython comptaibility
53-
while not self._flag:
54-
await asyncio.sleep_ms(self.delay_ms)
55-
56-
def __await__(self):
57-
while not self._flag:
58-
await asyncio.sleep_ms(self.delay_ms)
16+
# A coro or hard/soft ISR raising the message issues.set(payload)
17+
# .clear() should be issued by at least one waiting task and before
18+
# next event.
19+
20+
class Message(asyncio.ThreadSafeFlag):
21+
def __init__(self, _=0): # Arg: poll interval. Compatibility with old code.
22+
self._evt = asyncio.Event()
23+
self._data = None # Message
24+
self._state = False # Ensure only one task waits on ThreadSafeFlag
25+
self._is_set = False # For .is_set()
26+
super().__init__()
27+
28+
def clear(self): # At least one task must call clear when scheduled
29+
self._state = False
30+
self._is_set = False
31+
32+
def __iter__(self):
33+
yield from self.wait()
34+
return self._data
35+
36+
async def wait(self):
37+
if self._state: # A task waits on ThreadSafeFlag
38+
await self._evt.wait() # Wait on event
39+
else: # First task to wait
40+
self._state = True
41+
# Ensure other tasks see updated ._state before they wait
42+
await asyncio.sleep_ms(0)
43+
await super().wait() # Wait on ThreadSafeFlag
44+
self._evt.set()
45+
self._evt.clear()
46+
return self._data
5947

60-
__iter__ = __await__
48+
def set(self, data=None): # Can be called from a hard ISR
49+
self._data = data
50+
self._is_set = True
51+
super().set()
6152

6253
def is_set(self):
63-
return self._flag
64-
65-
def set(self, data=None):
66-
self._flag = True
67-
self._data = data
54+
return self._is_set
6855

6956
def value(self):
7057
return self._data

0 commit comments

Comments
 (0)