Skip to content

Commit 87b6cdf

Browse files
committed
THREADING.md: Further updata.
1 parent a378fec commit 87b6cdf

File tree

2 files changed

+136
-44
lines changed

2 files changed

+136
-44
lines changed

v3/docs/DRIVERS.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@ The `primitives.switch` module provides the `Switch` class. This supports
7272
debouncing a normally open switch connected between a pin and ground. Can run
7373
callbacks or schedule coros on contact closure and/or opening. As an
7474
alternative to a callback based interface, bound `Event` objects may be
75-
triggered on switch state changes.
75+
triggered on switch state changes. To use an `Event` based interface
76+
exclusively see the simpler [ESwitch class](./EVENTS.md#61-eswitch).
7677

7778
In the following text the term `callable` implies a Python `callable`: namely a
7879
function, bound method, coroutine or bound coroutine. The term implies that any
@@ -140,7 +141,8 @@ instance. A bound contact closure `Event` is created by passing `None` to
140141

141142
This is discussed further in
142143
[Event based interface](./DRIVERS.md#8-event-based-interface) which includes a
143-
code example. This API is recommended for new projects.
144+
code example. This API and the simpler [EButton class](./EVENTS.md#62-ebutton)
145+
is recommended for new projects.
144146

145147
###### [Contents](./DRIVERS.md#1-contents)
146148

@@ -156,13 +158,15 @@ double-click appears as four voltage changes. The asynchronous `Pushbutton`
156158
class provides the logic required to handle these user interactions by
157159
monitoring these events over time.
158160

159-
Instances of this class can run a `callable` on on press, release, double-click
160-
or long press events.
161+
Instances of this class can run a `callable` on press, release, double-click or
162+
long press events.
161163

162164
As an alternative to callbacks bound `Event` instances may be created which are
163165
triggered by press, release, double-click or long press events. This mode of
164166
operation is more flexible than the use of callbacks and is covered in
165-
[Event based interface](./DRIVERS.md#8-event-based-interface).
167+
[Event based interface](./DRIVERS.md#8-event-based-interface). To use an
168+
`Event` based interface exclusively see the simpler
169+
[EButton class](./EVENTS.md#62-ebutton).
166170

167171
## 4.1 Pushbutton class
168172

v3/docs/THREADING.md

Lines changed: 127 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ is fixed.
2626
1.2 [Soft Interrupt Service Routines](./THREADING.md#12-soft-interrupt-service-routines) Also code scheduled by micropython.schedule()
2727
1.3 [Threaded code on one core](./THREADING.md#13-threaded-code-on-one-core)
2828
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)
3031
2. [Sharing data](./THREADING.md#2-sharing-data)
3132
2.1 [A pool](./THREADING.md#21-a-pool) Sharing a set of variables.
3233
2.2 [ThreadSafeQueue](./THREADING.md#22-threadsafequeue)
@@ -36,6 +37,7 @@ is fixed.
3637
3.1 [Threadsafe Event](./THREADING.md#31-threadsafe-event)
3738
3.2 [Message](./THREADING.md#32-message) A threadsafe event with data payload.
3839
4. [Taming blocking functions](./THREADING.md#4-taming-blocking-functions)
40+
5. [Glossary](./THREADING.md#5-glossary) Terminology of realtime coding.
3941

4042
# 1. Introduction
4143

@@ -47,48 +49,47 @@ in a different context. Supported contexts are:
4749
4. Code running on a different core (currently only supported on RP2).
4850

4951
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).
5554

5655
This section compares the characteristics of the four contexts. Consider this
5756
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.)
5959
```python
6060
def update_dict():
6161
d["x"] = read_data(0)
6262
d["y"] = read_data(1)
6363
d["z"] = read_data(2)
6464
```
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.
7271

7372
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`.
7677

7778
## 1.1 Hard Interrupt Service Routines
7879

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).
8889
2. An ISR will run to completion before the main program regains control. This
8990
means that if the ISR updates multiple items, when the main program resumes,
9091
those items will be mutually consistent. The above code fragment will provide
91-
mutually consistent data.
92+
mutually consistent data (but see **consistency** below).
9293
3. The fact that ISR code runs to completion means that it must run fast to
9394
avoid disrupting the main program or delaying other ISR's. ISR code should not
9495
call blocking routines. It should not wait on locks because there is no way
@@ -118,11 +119,22 @@ async def foo():
118119
await process(a + b)
119120
```
120121
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+
```
122133

123134
## 1.2 Soft Interrupt Service Routines
124135

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.
126138

127139
1. A soft ISR can only run at certain bytecode boundaries, not during
128140
execution of a bytecode. It cannot interrupt garbage collection; this enables
@@ -146,7 +158,8 @@ This also includes code scheduled by `micropython.schedule()`.
146158
is needed to ensure that a read cannot be scheduled while an update is in
147159
progress.
148160
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.
150163
4. Code running on a thread other than that running `uasyncio` may block for
151164
as long as necessary (an application of threading is to handle blocking calls
152165
in a way that allows `uasyncio` to continue running).
@@ -164,17 +177,48 @@ thread safe classes offered here do not yet support Unix.
164177
is only required if mutual consistency of the three values is essential.
165178
3. In the absence of a GIL some operations on built-in objects are not thread
166179
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`
171182
interfacing apply.
172183
5. Code running on a core other than that running `uasyncio` may block for
173184
as long as necessary.
174185

175186
[See this reference from @jimmo](https://github.com/orgs/micropython/discussions/10135#discussioncomment-4309865).
176187

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
178222

179223
A key practical point is that coding errors in synchronising threads can be
180224
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
198242
are updated by one process and read by another, as the read might occur while
199243
the write operation is in progress.
200244

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:
202247
```python
203248
lock = _thread.allocate_lock()
204249
values = { "X": 0, "Y": 0, "Z": 0}
@@ -246,10 +291,10 @@ def producer():
246291
values["Y"] = sensor_read(1)
247292
values["Z"] = sensor_read(2)
248293
```
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.
253298

254299
###### [Contents](./THREADING.md#contents)
255300

@@ -632,3 +677,46 @@ async def main():
632677
asyncio.run(main())
633678
```
634679
###### [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

Comments
 (0)