@@ -26,7 +26,8 @@ is fixed.
26
26
1.2 [ Soft Interrupt Service Routines] ( ./THREADING.md#12-soft-interrupt-service-routines ) Also code scheduled by micropython.schedule()
27
27
1.3 [ Threaded code on one core] ( ./THREADING.md#13-threaded-code-on-one-core )
28
28
1.4 [ Threaded code on multiple cores] ( ./THREADING.md#14-threaded-code-on-multiple-cores )
29
- 1.5 [ Debugging] ( ./THREADING.md#15-debugging )
29
+ 1.5 [ Globals] ( ./THREADING.md#15-globals )
30
+ 1.6 [ Debugging] ( ./THREADING.md#16-debugging )
30
31
2 . [ Sharing data] ( ./THREADING.md#2-sharing-data )
31
32
2.1 [ A pool] ( ./THREADING.md#21-a-pool ) Sharing a set of variables.
32
33
2.2 [ ThreadSafeQueue] ( ./THREADING.md#22-threadsafequeue )
@@ -36,6 +37,7 @@ is fixed.
36
37
3.1 [ Threadsafe Event] ( ./THREADING.md#31-threadsafe-event )
37
38
3.2 [ Message] ( ./THREADING.md#32-message ) A threadsafe event with data payload.
38
39
4 . [ Taming blocking functions] ( ./THREADING.md#4-taming-blocking-functions )
40
+ 5 . [ Glossary] ( ./THREADING.md#5-glossary ) Terminology of realtime coding.
39
41
40
42
# 1. Introduction
41
43
@@ -47,48 +49,47 @@ in a different context. Supported contexts are:
47
49
4 . Code running on a different core (currently only supported on RP2).
48
50
49
51
In all these cases the contexts share a common VM (the virtual machine which
50
- executes Python bytecode). This enables the contexts to share global state. In
51
- case 4 there is no common GIL (the global interpreter lock). This lock protects
52
- Python built-in objects enabling them to be considered atomic at the bytecode
53
- level. (An "atomic" object is inherently thread safe: if thread changes it,
54
- another concurrent thread performing a read is guaranteed to see valid data).
52
+ executes Python bytecode). This enables the contexts to share global state. The
53
+ contexts differ in their use of the GIL [ see glossary] ( ./THREADING.md#5-glossary ) .
55
54
56
55
This section compares the characteristics of the four contexts. Consider this
57
56
function which updates a global dictionary ` d ` from a hardware device. The
58
- dictionary is shared with a ` uasyncio ` task.
57
+ dictionary is shared with a ` uasyncio ` task. (The function serves to illustrate
58
+ concurrency issues: it is not the most effcient way to transfer data.)
59
59
``` python
60
60
def update_dict ():
61
61
d[" x" ] = read_data(0 )
62
62
d[" y" ] = read_data(1 )
63
63
d[" z" ] = read_data(2 )
64
64
```
65
- This might be called in a soft ISR, in a thread running on the same core as
66
- ` uasyncio ` , or in a thread running on a different core. Each of these contexts
67
- has different characteristics, outlined below. In all these cases "thread safe"
68
- constructs are needed to interface ` uasyncio ` tasks with code running in these
69
- contexts. The official ` ThreadSafeFlag ` , or the classes documented here, may be
70
- used in all of these cases. This ` update_dict ` function serves to illustrate
71
- concurrency issues: it is not the most effcient way to transfer data.
65
+ This might be called in a hard or soft ISR, in a thread running on the same
66
+ core as ` uasyncio ` , or in a thread running on a different core. Each of these
67
+ contexts has different characteristics, outlined below. In all these cases
68
+ "thread safe" constructs are needed to interface ` uasyncio ` tasks with code
69
+ running in these contexts. The official ` ThreadSafeFlag ` , or the classes
70
+ documented here, may be used.
72
71
73
72
Beware that some apparently obvious ways to interface an ISR to ` uasyncio `
74
- introduce subtle bugs discussed in the doc referenced above. The only reliable
75
- interface is via a thread safe class, usually ` ThreadSafeFlag ` .
73
+ introduce subtle bugs discussed in
74
+ [ this doc] ( https://github.com/peterhinch/micropython-async/blob/master/v3/docs/INTERRUPTS.md )
75
+ referenced above. The only reliable interface is via a thread safe class,
76
+ usually ` ThreadSafeFlag ` .
76
77
77
78
## 1.1 Hard Interrupt Service Routines
78
79
79
- 1 . The ISR and the main program share the Python GIL. This ensures that built
80
- in Python objects ( ` list ` , ` dict ` etc.) will not be corrupted if an ISR runs
81
- while the object's contents are being modified. This guarantee is limited: the
82
- code will not crash, but there may be consistency problems. See consistency
83
- below. Further, failure can occur if the object's _ structure _ is modified, for
84
- example by the main program adding or deleting a dictionary entry. Note that
85
- globals are implemented as a ` dict ` . Globals should be declared before an ISR
86
- starts to run. Alternatively interrupts should be disabled while adding or
87
- deleting a global .
80
+ 1 . The ISR sees the GIL state of the main program: if the latter has locked
81
+ the GIL, the ISR will still run. This renders the GIL, as seen by the ISR,
82
+ ineffective. Built in Python objects ( ` list ` , ` dict ` etc.) will not be
83
+ corrupted if an ISR runs while the object's contents are being modified as
84
+ these updates are atomic. This guarantee is limited: the code will not crash,
85
+ but there may be consistency problems. See ** consistency ** below. The lack of GIL
86
+ functionality means that failure can occur if the object's _ structure _ is
87
+ modified, for example by the main program adding or deleting a dictionary
88
+ entry. This results in issues for [ globals ] ( ./THREADING.md#15-globals ) .
88
89
2 . An ISR will run to completion before the main program regains control. This
89
90
means that if the ISR updates multiple items, when the main program resumes,
90
91
those items will be mutually consistent. The above code fragment will provide
91
- mutually consistent data.
92
+ mutually consistent data (but see ** consistency ** below) .
92
93
3 . The fact that ISR code runs to completion means that it must run fast to
93
94
avoid disrupting the main program or delaying other ISR's. ISR code should not
94
95
call blocking routines. It should not wait on locks because there is no way
@@ -118,11 +119,22 @@ async def foo():
118
119
await process(a + b)
119
120
```
120
121
A hard ISR can occur during the execution of a bytecode. This means that the
121
- combined list passed to ` process() ` might comprise old a + new b.
122
+ combined list passed to ` process() ` might comprise old a + new b. Even though
123
+ the ISR produces consistent data, the fact that it can preempt the main code
124
+ at any time means that to read consistent data interrupts must be disabled:
125
+ ``` python
126
+ async def foo ():
127
+ while True :
128
+ state = machine.disable_irq()
129
+ d = a + b # Disable for as short a time as possible
130
+ machine.enable_irq(state)
131
+ await process(d)
132
+ ```
122
133
123
134
## 1.2 Soft Interrupt Service Routines
124
135
125
- This also includes code scheduled by ` micropython.schedule() ` .
136
+ This also includes code scheduled by ` micropython.schedule() ` which is assumed
137
+ to have been called from a hard ISR.
126
138
127
139
1 . A soft ISR can only run at certain bytecode boundaries, not during
128
140
execution of a bytecode. It cannot interrupt garbage collection; this enables
@@ -146,7 +158,8 @@ This also includes code scheduled by `micropython.schedule()`.
146
158
is needed to ensure that a read cannot be scheduled while an update is in
147
159
progress.
148
160
3 . The above means that, for example, calling ` uasyncio.create_task ` from a
149
- thread is unsafe as it can scramble ` uasyncio ` data structures.
161
+ thread is unsafe as it can destroy the mutual consistency of ` uasyncio ` data
162
+ structures.
150
163
4 . Code running on a thread other than that running ` uasyncio ` may block for
151
164
as long as necessary (an application of threading is to handle blocking calls
152
165
in a way that allows ` uasyncio ` to continue running).
@@ -164,17 +177,48 @@ thread safe classes offered here do not yet support Unix.
164
177
is only required if mutual consistency of the three values is essential.
165
178
3 . In the absence of a GIL some operations on built-in objects are not thread
166
179
safe. For example adding or deleting items in a ` dict ` . This extends to global
167
- variables which are implemented as a ` dict ` . Creating a new global on one core
168
- while another core reads a different global could fail in the event that the
169
- write operation triggered a re-hash. A lock should be used in such cases.
170
- 4 . The observations in 1.3 on user defined data structures and ` uasyncio `
180
+ variables which are implemented as a ` dict ` . See [ Globals] ( ./THREADING.md#15-globals ) .
181
+ 4 . The observations in 1.3 re user defined data structures and ` uasyncio `
171
182
interfacing apply.
172
183
5 . Code running on a core other than that running ` uasyncio ` may block for
173
184
as long as necessary.
174
185
175
186
[ See this reference from @jimmo ] ( https://github.com/orgs/micropython/discussions/10135#discussioncomment-4309865 ) .
176
187
177
- ## 1.5 Debugging
188
+ ## 1.5 Globals
189
+
190
+ Globals are implemented as a ` dict ` . Adding or deleting an entry is unsafe in
191
+ the main program if there is a context which accesses global data and does not
192
+ use the GIL. This means hard ISR's and code running on another core. Given that
193
+ shared global data is widely used, the following guidelines should be followed.
194
+
195
+ All globals should be declared in the main program before an ISR starts to run,
196
+ and before code on another core is started. It is valid to insert placeholder
197
+ data, as updates to ` dict ` data are atomic. In the example below, a pointer to
198
+ the ` None ` object is replaced by a pointer to a class instance: a pointer
199
+ update is atomic so can occur while globals are accessed by code in other
200
+ contexts.
201
+ ``` python
202
+ display_driver = None
203
+ # Start code on other core
204
+ # It's now valid to do
205
+ display_driver = DisplayDriverClass(args)
206
+ ```
207
+ The hazard with globals can occur in other ways. Importing a module while other
208
+ contexts are accessing globals can be problematic as that module might create
209
+ global objects. The following would present a hazard if ` foo ` were run for the
210
+ first time while globals were being accessed:
211
+ ``` python
212
+ def foo ():
213
+ global bar
214
+ bar = 42
215
+ ```
216
+ Once again the hazard is avoided by, in global scope, populating ` bar ` prior
217
+ with a placeholder before allowing other contexts to run.
218
+
219
+ If globals must be created and destroyed dynaically, a lock must be used.
220
+
221
+ ## 1.6 Debugging
178
222
179
223
A key practical point is that coding errors in synchronising threads can be
180
224
hard to locate: consequences can be extremely rare bugs or (in the case of
@@ -198,7 +242,8 @@ pointer, and replacing a pointer is atomic. Problems arise when multiple fields
198
242
are updated by one process and read by another, as the read might occur while
199
243
the write operation is in progress.
200
244
201
- One approach is to use locking:
245
+ One approach is to use locking. This example solves data sharing, but does not
246
+ address synchronisation:
202
247
``` python
203
248
lock = _thread.allocate_lock()
204
249
values = { " X" : 0 , " Y" : 0 , " Z" : 0 }
@@ -246,10 +291,10 @@ def producer():
246
291
values[" Y" ] = sensor_read(1 )
247
292
values[" Z" ] = sensor_read(2 )
248
293
```
249
- and the ISR would run to completion before ` uasyncio ` resumed. The ISR could
250
- run while the ` uasyncio ` task was reading the values: to ensure mutual
251
- consistency of the dict values the consumer should disable interrupts while
252
- the read is in progress.
294
+ and the ISR would run to completion before ` uasyncio ` resumed. However the ISR
295
+ might run while the ` uasyncio ` task was reading the values: to ensure mutual
296
+ consistency of the dict values the consumer should disable interrupts while the
297
+ read is in progress.
253
298
254
299
###### [ Contents] ( ./THREADING.md#contents )
255
300
@@ -632,3 +677,46 @@ async def main():
632
677
asyncio.run(main())
633
678
```
634
679
###### [ Contents] ( ./THREADING.md#contents )
680
+
681
+ # 5. Glossary
682
+
683
+ ### ISR
684
+
685
+ An Interrupt Service Routine: code that runs in response to an interrupt. Hard
686
+ ISR's offer very low latency but require careful coding - see
687
+ [ official docs] ( http://docs.micropython.org/en/latest/reference/isr_rules.html ) .
688
+
689
+ ### Context
690
+
691
+ In MicroPython terms a ` context ` may be viewed as a stream of bytecodes. A
692
+ ` uasyncio ` program comprises a single context: execution is passed between
693
+ tasks and the scheduler as a single stream of code. By contrast code in an ISR
694
+ can preempt the main stream to run its own stream. This is also true of threads
695
+ which can preempt each other at arbitrary times, and code on another core
696
+ which runs independently albeit under the same VM.
697
+
698
+ ### GIL
699
+
700
+ MicroPython has a Global Interpreter Lock. The purpose of this is to ensure
701
+ that multi-threaded programs cannot cause corruption in the event that two
702
+ contexts simultaneously modify an instance of a Python built-in class. It does
703
+ not protect user defined objects.
704
+
705
+ ### micropython.schedule
706
+
707
+ The relevance of this is that it is normally called in a hard ISR. In this
708
+ case the scheduled code runs in a different context to the main program. See
709
+ [ official docs] ( http://docs.micropython.org/en/latest/library/micropython.html#micropython.schedule ) .
710
+
711
+ ### VM
712
+
713
+ In MicroPython terms a VM is the Virtual Machine that executes bytecode. Code
714
+ running in different contexts share a common VM which enables the contexts to
715
+ share global objects.
716
+
717
+ ### Atomic
718
+
719
+ An operation is described as "atomic" if it can be guaranteed to proceed to
720
+ completion without being preempted. Writing an integer is atomic at the machine
721
+ code level. Updating a dictionary value is atomic at bytecode level. Adding or
722
+ deleting a dictionary key is not.
0 commit comments