Skip to content

Commit 4dfecdd

Browse files
committed
Message class: Remove redundant code.
1 parent b821096 commit 4dfecdd

File tree

3 files changed

+100
-56
lines changed

3 files changed

+100
-56
lines changed

v3/docs/THREADING.md

Lines changed: 96 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,42 @@
11
# Linking uasyncio and other contexts
22

3+
This document is primarily for those wishing to interface `uasyncio` code with
4+
that running under the `_thread` module. It presents classes for that purpose
5+
which may also find use for communicatiing between threads and in interrupt
6+
service routine (ISR) applications. It provides an overview of the problems
7+
implicit in pre-emptive multi tasking.
8+
9+
It is not an introduction into ISR coding. For this see
10+
[the official docs](http://docs.micropython.org/en/latest/reference/isr_rules.html)
11+
and [this doc](https://github.com/peterhinch/micropython-async/blob/master/v3/docs/INTERRUPTS.md)
12+
which provides specific guidance on interfacing `uasyncio` with ISR's.
13+
14+
# Contents
15+
16+
1. [Introduction](./THREADING.md#1-introduction) The various types of pre-emptive code.
17+
1.1 [Interrupt Service Routines](./THREADING.md#11-interrupt-service-routines)
18+
1.2 [Threaded code on one core](./THREADING.md#12-threaded-code-on-one-core)
19+
1.3 [Threaded code on multiple cores](./THREADING.md#13-threaded-code-on-multiple-cores)
20+
1.4 [Debugging](./THREADING.md#14-debugging)
21+
2. [Sharing data](./THREADING.md#2-sharing-data)
22+
2.1 [A pool](./THREADING.md#21-a-pool) Sharing a set of variables.
23+
2.2 [ThreadSafeQueue](./THREADING.md#22-threadsafequeue)
24+
     2.2.1 [Blocking](./THREADING.md#221-blocking)
25+
     2.2.3 [Object ownership](./THREADING.md#223-object-ownership)
26+
3. [Synchronisation](./THREADING.md#3-synchronisation)
27+
3.1 [Threadsafe Event](./THREADING.md#31-threadsafe-event)
28+
329
# 1. Introduction
430

5-
This document identifies issues arising when `uasyncio` applications interface
6-
code running in a different context. Supported contexts are:
7-
1. An interrupt service routine (ISR).
31+
Various issues arise when `uasyncio` applications interface with code running
32+
in a different context. Supported contexts are:
33+
1. A hard or soft interrupt service routine (ISR).
834
2. Another thread running on the same core.
935
3. Code running on a different core (currently only supported on RP2).
1036

11-
Note that hard ISR's require careful coding to avoid RAM allocation. See
12-
[the official docs](http://docs.micropython.org/en/latest/reference/isr_rules.html).
13-
The allocation issue is orthogonal to the concurrency issues discussed in this
14-
document. Concurrency problems apply equally to hard and soft ISR's. Code
15-
samples assume a soft ISR or a function launched by `micropython.schedule`.
16-
[This doc](https://github.com/peterhinch/micropython-async/blob/master/v3/docs/INTERRUPTS.md)
17-
provides specific guidance on interfacing `uasyncio` with ISR's.
18-
19-
The rest of this section compares the characteristics of the three contexts.
20-
Consider this function which updates a global dictionary `d` from a hardware
21-
device. The dictionary is shared with a `uasyncio` task.
37+
This section compares the characteristics of the three contexts. Consider this
38+
function which updates a global dictionary `d` from a hardware device. The
39+
dictionary is shared with a `uasyncio` task.
2240
```python
2341
def update_dict():
2442
d["x"] = read_data(0)
@@ -30,40 +48,41 @@ This might be called in a soft ISR, in a thread running on the same core as
3048
has different characteristics, outlined below. In all these cases "thread safe"
3149
constructs are needed to interface `uasyncio` tasks with code running in these
3250
contexts. The official `ThreadSafeFlag`, or the classes documented here, may be
33-
used in all of these cases. This function serves to illustrate concurrency
34-
issues: it is not the most effcient way to transfer data.
51+
used in all of these cases. This `update_dict` function serves to illustrate
52+
concurrency issues: it is not the most effcient way to transfer data.
3553

3654
Beware that some apparently obvious ways to interface an ISR to `uasyncio`
3755
introduce subtle bugs discussed in the doc referenced above. The only reliable
38-
interface is via a thread safe class.
56+
interface is via a thread safe class, usually `ThreadSafeFlag`.
3957

40-
## 1.1 Soft Interrupt Service Routines
58+
## 1.1 Interrupt Service Routines
4159

4260
1. The ISR and the main program share a common Python virtual machine (VM).
4361
Consequently a line of code being executed when the interrupt occurs will run
4462
to completion before the ISR runs.
4563
2. An ISR will run to completion before the main program regains control. This
4664
means that if the ISR updates multiple items, when the main program resumes,
47-
those items will be mutually consistent. The above code fragment will work
48-
unchanged.
65+
those items will be mutually consistent. The above code fragment will provide
66+
mutually consistent data.
4967
3. The fact that ISR code runs to completion means that it must run fast to
5068
avoid disrupting the main program or delaying other ISR's. ISR code should not
5169
call blocking routines and should not wait on locks. Item 2. means that locks
52-
are not usually necessary.
70+
are seldom necessary.
5371
4. If a burst of interrupts can occur faster than `uasyncio` can schedule the
5472
handling task, data loss can occur. Consider using a `ThreadSafeQueue`. Note
55-
that if this high rate is sustained something will break and the overall
56-
design needs review. It may be necessary to discard some data items.
73+
that if this high rate is sustained something will break: the overall design
74+
needs review. It may be necessary to discard some data items.
5775

5876
## 1.2 Threaded code on one core
5977

60-
1. Both contexts share a common VM so Python code integrity is guaranteed.
61-
2. If one thread updates a data item there is no risk of the main program
62-
reading a corrupt or partially updated item. If such code updates multiple
63-
shared data items, note that `uasyncio` can regain control at any time. The
64-
above code fragment may not have updated all the dictionary keys when
65-
`uasyncio` regains control. If mutual consistency is important, a lock or
66-
`ThreadSafeQueue` must be used.
78+
1. Behaviour depends on the port
79+
[see](https://github.com/micropython/micropython/discussions/10135#discussioncomment-4275354).
80+
At best, context switches can occur at bytecode boundaries. On ports where
81+
contexts share no GIL they can occur at any time.
82+
2. Hence for shared data item more complex than a small int, a lock or
83+
`ThreadSafeQueue` must be used. This ensures that the thread reading the data
84+
cannot access a partially updated item (which might even result in a crash).
85+
It also ensures mutual consistency between multiple data items.
6786
3. Code running on a thread other than that running `uasyncio` may block for
6887
as long as necessary (an application of threading is to handle blocking calls
6988
in a way that allows `uasyncio` to continue running).
@@ -79,21 +98,28 @@ interface is via a thread safe class.
7998
3. Code running on a core other than that running `uasyncio` may block for
8099
as long as necessary.
81100

101+
## 1.4 Debugging
102+
82103
A key practical point is that coding errors in synchronising threads can be
83-
hard to locate: consequences can be extremely rare bugs or crashes. It is vital
84-
to be careful in the way that communication between the contexts is achieved. This
85-
doc aims to provide some guidelines and code to assist in this task.
104+
hard to locate: consequences can be extremely rare bugs or (in the case of
105+
multi-core systems) crashes. It is vital to be careful in the way that
106+
communication between the contexts is achieved. This doc aims to provide some
107+
guidelines and code to assist in this task.
86108

87109
There are two fundamental problems: data sharing and synchronisation.
88110

89-
# 2. Data sharing
111+
###### [Contents](./THREADING.md#contents)
112+
113+
# 2. Sharing data
114+
115+
## 2.1 A pool
90116

91117
The simplest case is a shared pool of data. It is possible to share an `int` or
92118
`bool` because at machine code level writing an `int` is "atomic": it cannot be
93-
interrupted. Anything more complex must be protected to ensure that concurrent
94-
access cannot take place. The consequences even of reading an object while it
95-
is being written can be unpredictable. One approach is to use locking:
96-
119+
interrupted. In the multi core case anything more complex must be protected to
120+
ensure that concurrent access cannot take place. The consequences even of
121+
reading an object while it is being written can be unpredictable. One approach
122+
is to use locking:
97123
```python
98124
lock = _thread.allocate_lock()
99125
values = { "X": 0, "Y": 0, "Z": 0}
@@ -113,18 +139,30 @@ async def consumer():
113139
lock.acquire()
114140
await process(values) # Do something with the data
115141
lock.release()
142+
await asyncio.sleep_ms(0) # Ensure producer has time to grab the lock
116143
```
117-
This will work even for the multi core case. However the consumer might hold
118-
the lock for some time: it will take time for the scheduler to execute the
119-
`process()` call, and the call itself will take time to run. This would be
120-
problematic if the producer were an ISR. In this case the absence of a lock
121-
would not result in crashes because an ISR cannot interrupt a MicroPython
122-
instruction.
144+
This is recommended where the producer runs in a different thread from
145+
`uasyncio`. However the consumer might hold the lock for some time: it will
146+
take time for the scheduler to execute the `process()` call, and the call
147+
itself will take time to run. In cases where the duration of a lock is
148+
problematic a `ThreadSafeQueue` is more appropriate as it decouples producer
149+
and consumer code.
150+
151+
As stated above, if the producer is an ISR no lock is needed or advised.
152+
Producer code would follow this pattern:
153+
```python
154+
values = { "X": 0, "Y": 0, "Z": 0}
155+
def producer():
156+
values["X"] = sensor_read(0)
157+
values["Y"] = sensor_read(1)
158+
values["Z"] = sensor_read(2)
159+
```
160+
and the ISR would run to completion before `uasyncio` resumed, ensuring mutual
161+
consistency of the dict values.
123162

124-
In cases where the duration of a lock is problematic a `ThreadSafeQueue` is
125-
more appropriate as it decouples producer and consumer code.
163+
###### [Contents](./THREADING.md#contents)
126164

127-
## 2.1 ThreadSafeQueue
165+
## 2.2 ThreadSafeQueue
128166

129167
This queue is designed to interface between one `uasyncio` task and a single
130168
thread running in a different context. This can be an interrupt service routine
@@ -203,7 +241,9 @@ while True:
203241
process(data) # Do something with it
204242
```
205243

206-
### 2.1.1 Blocking
244+
###### [Contents](./THREADING.md#contents)
245+
246+
### 2.2.1 Blocking
207247

208248
These methods, called with `blocking=False`, produce an immediate return. To
209249
avoid an `IndexError` the user should check for full or empty status before
@@ -215,7 +255,9 @@ non-`uasyncio ` context. If invoked in a `uasyncio` task they must not be
215255
allowed to block because it would lock up the scheduler. Nor should they be
216256
allowed to block in an ISR where blocking can have unpredictable consequences.
217257

218-
### 2.1.2 Object ownership
258+
###### [Contents](./THREADING.md#contents)
259+
260+
### 2.2.2 Object ownership
219261

220262
Any Python object can be placed on a queue, but the user should be aware that
221263
once the producer puts an object on the queue it loses ownership of the object
@@ -243,7 +285,9 @@ using objects requires the producer to be notified that the consumer has
243285
finished with the item. In general it is simpler to create new objects and let
244286
the MicroPython garbage collector delete them as per the first sample.
245287

246-
### 2.1.3 A complete example
288+
###### [Contents](./THREADING.md#contents)
289+
290+
### 2.2.3 A complete example
247291

248292
This demonstrates an echo server running on core 2. The `sender` task sends
249293
consecutive integers to the server, which echoes them back on a second queue.
@@ -282,6 +326,8 @@ async def main():
282326

283327
asyncio.run(main())
284328
```
329+
###### [Contents](./THREADING.md#contents)
330+
285331
# 3. Synchronisation
286332

287333
The principal means of synchronising `uasyncio` code with that running in

v3/docs/TUTORIAL.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2968,4 +2968,7 @@ The above comments refer to an ideal scheduler. Currently `uasyncio` is not in
29682968
this category, with worst-case latency being > `N`ms. The conclusions remain
29692969
valid.
29702970

2971+
This, along with other issues, is discussed in
2972+
[Interfacing uasyncio to interrupts](./INTERRUPTS.md).
2973+
29712974
###### [Contents](./TUTORIAL.md#contents)

v3/primitives/message.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,8 @@ def __init__(self):
2828
self._waiting_on_tsf = False
2929
self._tsf = asyncio.ThreadSafeFlag()
3030
self._data = None # Message
31-
self._is_set = False
3231

3332
def clear(self): # At least one task must call clear when scheduled
34-
self._is_set = False
3533
super().clear()
3634

3735
def __iter__(self):
@@ -60,7 +58,7 @@ async def wait(self):
6058

6159
def set(self, data=None): # Can be called from a hard ISR
6260
self._data = data
63-
self._is_set = True
61+
super().set()
6462
self._tsf.set()
6563

6664
def __aiter__(self):
@@ -69,8 +67,5 @@ def __aiter__(self):
6967
async def __anext__(self):
7068
return await self
7169

72-
def is_set(self):
73-
return self._is_set
74-
7570
def value(self):
7671
return self._data

0 commit comments

Comments
 (0)