1
1
# Linking uasyncio and other contexts
2
2
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
+
3
29
# 1. Introduction
4
30
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).
8
34
2 . Another thread running on the same core.
9
35
3 . Code running on a different core (currently only supported on RP2).
10
36
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.
22
40
``` python
23
41
def update_dict ():
24
42
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
30
48
has different characteristics, outlined below. In all these cases "thread safe"
31
49
constructs are needed to interface ` uasyncio ` tasks with code running in these
32
50
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.
35
53
36
54
Beware that some apparently obvious ways to interface an ISR to ` uasyncio `
37
55
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 ` .
39
57
40
- ## 1.1 Soft Interrupt Service Routines
58
+ ## 1.1 Interrupt Service Routines
41
59
42
60
1 . The ISR and the main program share a common Python virtual machine (VM).
43
61
Consequently a line of code being executed when the interrupt occurs will run
44
62
to completion before the ISR runs.
45
63
2 . An ISR will run to completion before the main program regains control. This
46
64
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 .
49
67
3 . The fact that ISR code runs to completion means that it must run fast to
50
68
avoid disrupting the main program or delaying other ISR's. ISR code should not
51
69
call blocking routines and should not wait on locks. Item 2. means that locks
52
- are not usually necessary.
70
+ are seldom necessary.
53
71
4 . If a burst of interrupts can occur faster than ` uasyncio ` can schedule the
54
72
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.
57
75
58
76
## 1.2 Threaded code on one core
59
77
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.
67
86
3 . Code running on a thread other than that running ` uasyncio ` may block for
68
87
as long as necessary (an application of threading is to handle blocking calls
69
88
in a way that allows ` uasyncio ` to continue running).
@@ -79,21 +98,28 @@ interface is via a thread safe class.
79
98
3 . Code running on a core other than that running ` uasyncio ` may block for
80
99
as long as necessary.
81
100
101
+ ## 1.4 Debugging
102
+
82
103
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.
86
108
87
109
There are two fundamental problems: data sharing and synchronisation.
88
110
89
- # 2. Data sharing
111
+ ###### [ Contents] ( ./THREADING.md#contents )
112
+
113
+ # 2. Sharing data
114
+
115
+ ## 2.1 A pool
90
116
91
117
The simplest case is a shared pool of data. It is possible to share an ` int ` or
92
118
` 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:
97
123
``` python
98
124
lock = _thread.allocate_lock()
99
125
values = { " X" : 0 , " Y" : 0 , " Z" : 0 }
@@ -113,18 +139,30 @@ async def consumer():
113
139
lock.acquire()
114
140
await process(values) # Do something with the data
115
141
lock.release()
142
+ await asyncio.sleep_ms(0 ) # Ensure producer has time to grab the lock
116
143
```
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.
123
162
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 )
126
164
127
- ## 2.1 ThreadSafeQueue
165
+ ## 2.2 ThreadSafeQueue
128
166
129
167
This queue is designed to interface between one ` uasyncio ` task and a single
130
168
thread running in a different context. This can be an interrupt service routine
@@ -203,7 +241,9 @@ while True:
203
241
process(data) # Do something with it
204
242
```
205
243
206
- ### 2.1.1 Blocking
244
+ ###### [ Contents] ( ./THREADING.md#contents )
245
+
246
+ ### 2.2.1 Blocking
207
247
208
248
These methods, called with ` blocking=False ` , produce an immediate return. To
209
249
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
215
255
allowed to block because it would lock up the scheduler. Nor should they be
216
256
allowed to block in an ISR where blocking can have unpredictable consequences.
217
257
218
- ### 2.1.2 Object ownership
258
+ ###### [ Contents] ( ./THREADING.md#contents )
259
+
260
+ ### 2.2.2 Object ownership
219
261
220
262
Any Python object can be placed on a queue, but the user should be aware that
221
263
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
243
285
finished with the item. In general it is simpler to create new objects and let
244
286
the MicroPython garbage collector delete them as per the first sample.
245
287
246
- ### 2.1.3 A complete example
288
+ ###### [ Contents] ( ./THREADING.md#contents )
289
+
290
+ ### 2.2.3 A complete example
247
291
248
292
This demonstrates an echo server running on core 2. The ` sender ` task sends
249
293
consecutive integers to the server, which echoes them back on a second queue.
@@ -282,6 +326,8 @@ async def main():
282
326
283
327
asyncio.run(main())
284
328
```
329
+ ###### [ Contents] ( ./THREADING.md#contents )
330
+
285
331
# 3. Synchronisation
286
332
287
333
The principal means of synchronising ` uasyncio ` code with that running in
0 commit comments