@@ -8,18 +8,81 @@ code running in a different context. Supported contexts are:
8
8
2 . Another thread running on the same core.
9
9
3 . Code running on a different core (currently only supported on RP2).
10
10
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.
23
86
24
87
There are two fundamental problems: data sharing and synchronisation.
25
88
@@ -54,51 +117,14 @@ async def consumer():
54
117
This will work even for the multi core case. However the consumer might hold
55
118
the lock for some time: it will take time for the scheduler to execute the
56
119
` 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.
61
123
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.
63
126
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
102
128
103
129
This queue is designed to interface between one ` uasyncio ` task and a single
104
130
thread running in a different context. This can be an interrupt service routine
@@ -177,7 +203,7 @@ while True:
177
203
process(data) # Do something with it
178
204
```
179
205
180
- ## 3 .1 Blocking
206
+ ### 2.1 .1 Blocking
181
207
182
208
These methods, called with ` blocking=False ` , produce an immediate return. To
183
209
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
189
215
allowed to block because it would lock up the scheduler. Nor should they be
190
216
allowed to block in an ISR where blocking can have unpredictable consequences.
191
217
192
- ## 3 .2 Object ownership
218
+ ### 2.1 .2 Object ownership
193
219
194
220
Any Python object can be placed on a queue, but the user should be aware that
195
221
once the producer puts an object on the queue it loses ownership of the object
@@ -214,9 +240,10 @@ def get_coordinates(q):
214
240
The problem here is that the array is modified after being put on the queue. If
215
241
the queue is capable of holding 10 objects, 10 array instances are required. Re
216
242
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.
218
245
219
- ## 3 .3 A complete example
246
+ ### 2.1 .3 A complete example
220
247
221
248
This demonstrates an echo server running on core 2. The ` sender ` task sends
222
249
consecutive integers to the server, which echoes them back on a second queue.
@@ -255,3 +282,51 @@ async def main():
255
282
256
283
asyncio.run(main())
257
284
```
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