Skip to content

Commit b821096

Browse files
committed
THREADING.md: Improve section 1.
1 parent f95705d commit b821096

File tree

2 files changed

+143
-61
lines changed

2 files changed

+143
-61
lines changed

v3/docs/INTERRUPTS.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -185,9 +185,16 @@ async def process_data():
185185
# Process the data here before waiting for the next interrupt
186186
```
187187

188+
## 3.4 Thread Safe Classes
189+
190+
Other classes capable of being used to interface an ISR with `uasyncio` are
191+
discussed [here](https://github.com/peterhinch/micropython-async/blob/master/v3/docs/THREADING.md),
192+
notably the `ThreadSafeQueue`.
193+
188194
# 4. Conclusion
189195

190-
The key take-away is that `ThreadSafeFlag` is the only `uasyncio` construct
191-
which can safely be used in an ISR context.
196+
The key take-away is that `ThreadSafeFlag` is the only official `uasyncio`
197+
construct which can safely be used in an ISR context. Unofficial "thread
198+
safe" classes may also be used.
192199

193200
###### [Main tutorial](./TUTORIAL.md#contents)

v3/docs/THREADING.md

Lines changed: 134 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,81 @@ code running in a different context. Supported contexts are:
88
2. Another thread running on the same core.
99
3. Code running on a different core (currently only supported on RP2).
1010

11-
The first two cases are relatively straightforward because both contexts share
12-
a common bytecode interpreter and GIL. There is a guarantee that even a hard
13-
MicroPython (MP) ISR will not interrupt execution of a line of Python code.
14-
15-
This is not the case where the threads run on different cores, where there is
16-
no synchronisation between the streams of machine code. If the two threads
17-
concurrently modify a shared Python object it is possible that corruption will
18-
occur. Reading an object while it is being written can also produce an
19-
unpredictable outcome.
20-
21-
A key practical point is that coding errors can be hard to identify: the
22-
consequences can be extremely rare bugs or crashes.
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.
22+
```python
23+
def update_dict():
24+
d["x"] = read_data(0)
25+
d["y"] = read_data(1)
26+
d["z"] = read_data(2)
27+
```
28+
This might be called in a soft ISR, in a thread running on the same core as
29+
`uasyncio`, or in a thread running on a different core. Each of these contexts
30+
has different characteristics, outlined below. In all these cases "thread safe"
31+
constructs are needed to interface `uasyncio` tasks with code running in these
32+
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.
35+
36+
Beware that some apparently obvious ways to interface an ISR to `uasyncio`
37+
introduce subtle bugs discussed in the doc referenced above. The only reliable
38+
interface is via a thread safe class.
39+
40+
## 1.1 Soft Interrupt Service Routines
41+
42+
1. The ISR and the main program share a common Python virtual machine (VM).
43+
Consequently a line of code being executed when the interrupt occurs will run
44+
to completion before the ISR runs.
45+
2. An ISR will run to completion before the main program regains control. This
46+
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.
49+
3. The fact that ISR code runs to completion means that it must run fast to
50+
avoid disrupting the main program or delaying other ISR's. ISR code should not
51+
call blocking routines and should not wait on locks. Item 2. means that locks
52+
are not usually necessary.
53+
4. If a burst of interrupts can occur faster than `uasyncio` can schedule the
54+
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.
57+
58+
## 1.2 Threaded code on one core
59+
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.
67+
3. Code running on a thread other than that running `uasyncio` may block for
68+
as long as necessary (an application of threading is to handle blocking calls
69+
in a way that allows `uasyncio` to continue running).
70+
71+
## 1.3 Threaded code on multiple cores
72+
73+
1. There is no common VM. The underlying machine code of each core runs
74+
independently.
75+
2. In the code sample there is a risk of the `uasyncio` task reading the dict
76+
at the same moment as it is being written. It may read a corrupt or partially
77+
updated item; there may even be a crash. Using a lock or `ThreadSafeQueue` is
78+
essential.
79+
3. Code running on a core other than that running `uasyncio` may block for
80+
as long as necessary.
81+
82+
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.
2386

2487
There are two fundamental problems: data sharing and synchronisation.
2588

@@ -54,51 +117,14 @@ async def consumer():
54117
This will work even for the multi core case. However the consumer might hold
55118
the lock for some time: it will take time for the scheduler to execute the
56119
`process()` call, and the call itself will take time to run. This would be
57-
problematic if the producer were an ISR.
58-
59-
In cases such as this a `ThreadSafeQueue` is more appropriate as it decouples
60-
producer and consumer code.
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.
61123

62-
# 2. Threadsafe Event
124+
In cases where the duration of a lock is problematic a `ThreadSafeQueue` is
125+
more appropriate as it decouples producer and consumer code.
63126

64-
The `ThreadsafeFlag` has a limitation in that only a single task can wait on
65-
it. The `ThreadSafeEvent` overcomes this. It is subclassed from `Event` and
66-
presents the same interface. The `set` method may be called from an ISR or from
67-
code running on another core. Any number of tasks may wait on it.
68-
69-
The following Pyboard-specific code demos its use in a hard ISR:
70-
```python
71-
import uasyncio as asyncio
72-
from threadsafe import ThreadSafeEvent
73-
from pyb import Timer
74-
75-
async def waiter(n, evt):
76-
try:
77-
await evt.wait()
78-
print(f"Waiter {n} got event")
79-
except asyncio.CancelledError:
80-
print(f"Waiter {n} cancelled")
81-
82-
async def can(task):
83-
await asyncio.sleep_ms(100)
84-
task.cancel()
85-
86-
async def main():
87-
evt = ThreadSafeEvent()
88-
tim = Timer(4, freq=1, callback=lambda t: evt.set())
89-
nt = 0
90-
while True:
91-
tasks = [asyncio.create_task(waiter(n + 1, evt)) for n in range(4)]
92-
asyncio.create_task(can(tasks[nt]))
93-
await asyncio.gather(*tasks, return_exceptions=True)
94-
evt.clear()
95-
print("Cleared event")
96-
nt = (nt + 1) % 4
97-
98-
asyncio.run(main())
99-
```
100-
101-
# 3. Threadsafe Queue
127+
## 2.1 ThreadSafeQueue
102128

103129
This queue is designed to interface between one `uasyncio` task and a single
104130
thread running in a different context. This can be an interrupt service routine
@@ -177,7 +203,7 @@ while True:
177203
process(data) # Do something with it
178204
```
179205

180-
## 3.1 Blocking
206+
### 2.1.1 Blocking
181207

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

192-
## 3.2 Object ownership
218+
### 2.1.2 Object ownership
193219

194220
Any Python object can be placed on a queue, but the user should be aware that
195221
once the producer puts an object on the queue it loses ownership of the object
@@ -214,9 +240,10 @@ def get_coordinates(q):
214240
The problem here is that the array is modified after being put on the queue. If
215241
the queue is capable of holding 10 objects, 10 array instances are required. Re
216242
using objects requires the producer to be notified that the consumer has
217-
finished with the item.
243+
finished with the item. In general it is simpler to create new objects and let
244+
the MicroPython garbage collector delete them as per the first sample.
218245

219-
## 3.3 A complete example
246+
### 2.1.3 A complete example
220247

221248
This demonstrates an echo server running on core 2. The `sender` task sends
222249
consecutive integers to the server, which echoes them back on a second queue.
@@ -255,3 +282,51 @@ async def main():
255282

256283
asyncio.run(main())
257284
```
285+
# 3. Synchronisation
286+
287+
The principal means of synchronising `uasyncio` code with that running in
288+
another context is the `ThreadsafeFlag`. This is discussed in the
289+
[official docs](http://docs.micropython.org/en/latest/library/uasyncio.html#class-threadsafeflag)
290+
and [tutorial](https://github.com/peterhinch/micropython-async/blob/master/v3/docs/TUTORIAL.md#36-threadsafeflag).
291+
In essence a single `uasyncio` task waits on a shared `ThreadSafeEvent`. Code
292+
running in another context sets the flag. When the scheduler regains control
293+
and other pending tasks have run, the waiting task resumes.
294+
295+
## 3.1 Threadsafe Event
296+
297+
The `ThreadsafeFlag` has a limitation in that only a single task can wait on
298+
it. The `ThreadSafeEvent` overcomes this. It is subclassed from `Event` and
299+
presents the same interface. The `set` method may be called from an ISR or from
300+
code running on another core. Any number of tasks may wait on it.
301+
302+
The following Pyboard-specific code demos its use in a hard ISR:
303+
```python
304+
import uasyncio as asyncio
305+
from threadsafe import ThreadSafeEvent
306+
from pyb import Timer
307+
308+
async def waiter(n, evt):
309+
try:
310+
await evt.wait()
311+
print(f"Waiter {n} got event")
312+
except asyncio.CancelledError:
313+
print(f"Waiter {n} cancelled")
314+
315+
async def can(task):
316+
await asyncio.sleep_ms(100)
317+
task.cancel()
318+
319+
async def main():
320+
evt = ThreadSafeEvent()
321+
tim = Timer(4, freq=1, callback=lambda t: evt.set())
322+
nt = 0
323+
while True:
324+
tasks = [asyncio.create_task(waiter(n + 1, evt)) for n in range(4)]
325+
asyncio.create_task(can(tasks[nt]))
326+
await asyncio.gather(*tasks, return_exceptions=True)
327+
evt.clear()
328+
print("Cleared event")
329+
nt = (nt + 1) % 4
330+
331+
asyncio.run(main())
332+
```

0 commit comments

Comments
 (0)