Skip to content

Commit 5557622

Browse files
committed
Tutorial: Add section on threaded code.
1 parent f7c44fa commit 5557622

File tree

2 files changed

+48
-55
lines changed

2 files changed

+48
-55
lines changed

v3/docs/THREADING.md

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -82,29 +82,32 @@ interface is via a thread safe class, usually `ThreadSafeFlag`.
8282

8383
## 1.2 Threaded code on one core
8484

85-
1. Behaviour depends on the port
86-
[see](https://github.com/micropython/micropython/discussions/10135#discussioncomment-4275354).
87-
At best, context switches can occur at bytecode boundaries. On ports where
88-
contexts share no GIL they can occur at any time.
89-
2. Hence for shared data item more complex than a small int, a lock or
90-
`ThreadSafeQueue` must be used. This ensures that the thread reading the data
91-
cannot access a partially updated item (which might even result in a crash).
92-
It also ensures mutual consistency between multiple data items.
85+
1. On single core devices with a common GIL, Python instructions can be
86+
considered "atomic": they are guaranteed to run to completion without being
87+
pre-empted.
88+
2. Hence where a shared data item is updated by a single line of code a lock or
89+
`ThreadSafeQueue` is not needed. In the above code sample, if the application
90+
needs mutual consistency between the dictionary values, a lock must be used.
9391
3. Code running on a thread other than that running `uasyncio` may block for
9492
as long as necessary (an application of threading is to handle blocking calls
9593
in a way that allows `uasyncio` to continue running).
9694

9795
## 1.3 Threaded code on multiple cores
9896

99-
1. There is no common VM. The underlying machine code of each core runs
100-
independently.
97+
Currently this applies to RP2 and Unix ports, although as explained above the
98+
thread safe classes offered here do not yet support Unix.
99+
100+
1. There is no common VM hence no common GIL. The underlying machine code of
101+
each core runs independently.
101102
2. In the code sample there is a risk of the `uasyncio` task reading the dict
102103
at the same moment as it is being written. It may read a corrupt or partially
103104
updated item; there may even be a crash. Using a lock or `ThreadSafeQueue` is
104105
essential.
105106
3. Code running on a core other than that running `uasyncio` may block for
106107
as long as necessary.
107108

109+
[See this reference from @jimmo](https://github.com/orgs/micropython/discussions/10135#discussioncomment-4309865).
110+
108111
## 1.4 Debugging
109112

110113
A key practical point is that coding errors in synchronising threads can be

v3/docs/TUTORIAL.md

Lines changed: 35 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,10 @@ including device drivers, debugging aids, and documentation.
8484
8.4 [Scheduling in uasyncio](./TUTORIAL.md#84-scheduling-in-uasyncio)
8585
8.5 [Why cooperative rather than pre-emptive?](./TUTORIAL.md#85-why-cooperative-rather-than-pre-emptive)
8686
8.6 [Communication](./TUTORIAL.md#86-communication)
87-
9. [Polling vs Interrupts](./TUTORIAL.md#9-polling-vs-interrupts) A common
88-
source of confusion.
87+
9. [Polling vs Interrupts](./TUTORIAL.md#9-polling-vs-interrupts) A common
88+
source of confusion.
89+
10. [Interfacing threaded code](./TUTORIAL.md#10-interfacing-threaded-code) Taming blocking functions. Multi core coding.
90+
8991

9092
###### [Main README](../README.md)
9193

@@ -947,18 +949,23 @@ is raised.
947949

948950
## 3.5 Queue
949951

950-
This is currently an unofficial implementation. Its API is a subset of that of
951-
CPython's `asyncio.Queue`. Like `asyncio.Queue` this class is not thread safe.
952-
A queue class optimised for MicroPython is presented in
953-
[Ringbuf queue](./EVENTS.md#7-ringbuf-queue).
952+
Queue objects provide a means of synchronising producer and consumer tasks: the
953+
producer puts data items onto the queue with the consumer removing them. If the
954+
queue becomes full, the producer task will block, likewise if the queue becomes
955+
empty the consumer will block. Some queue implementations allow producer and
956+
consumer to run in different contexts: for example where one runs in an
957+
interrupt service routine or on a different thread or core from the `uasyncio`
958+
application. Such a queue is termed "thread safe".
954959

955-
The `Queue` class provides a means of synchronising producer and consumer
956-
tasks: the producer puts data items onto the queue with the consumer removing
957-
them. If the queue becomes full, the producer task will block, likewise if
958-
the queue becomes empty the consumer will block.
960+
The `Queue` class is an unofficial implementation whose API is a subset of that
961+
of CPython's `asyncio.Queue`. Like `asyncio.Queue` this class is not thread
962+
safe. A queue class optimised for MicroPython is presented in
963+
[Ringbuf queue](./EVENTS.md#7-ringbuf-queue). A thread safe version is
964+
documented in [ThreadSafeQueue](./THREADING.md#22-threadsafequeue).
959965

960-
Constructor: Optional arg `maxsize=0`. If zero, the queue can grow without
961-
limit subject to heap size. If >0 the queue's size will be constrained.
966+
Constructor:
967+
Optional arg `maxsize=0`. If zero, the queue can grow without limit subject to
968+
heap size. If `maxsize>0` the queue's size will be constrained.
962969

963970
Synchronous methods (immediate return):
964971
* `qsize` No arg. Returns the number of items in the queue.
@@ -1093,39 +1100,8 @@ hardware device requires the use of an ISR for a μs level response. Having
10931100
serviced the device, the ISR flags an asynchronous routine, typically
10941101
processing received data.
10951102

1096-
The fact that only one task may wait on a `ThreadSafeFlag` may be addressed as
1097-
follows.
1098-
```python
1099-
class ThreadSafeEvent(asyncio.Event):
1100-
def __init__(self):
1101-
super().__init__()
1102-
self._waiting_on_tsf = False
1103-
self._tsf = asyncio.ThreadSafeFlag()
1104-
1105-
def set(self):
1106-
self._tsf.set()
1107-
1108-
async def _waiter(self): # Runs if 1st task is cancelled
1109-
await self._tsf.wait()
1110-
super().set()
1111-
self._waiting_on_tsf = False
1112-
1113-
async def wait(self):
1114-
if self._waiting_on_tsf == False:
1115-
self._waiting_on_tsf = True
1116-
await asyncio.sleep(0) # Ensure other tasks see updated flag
1117-
try:
1118-
await self._tsf.wait()
1119-
super().set()
1120-
self._waiting_on_tsf = False
1121-
except asyncio.CancelledError:
1122-
asyncio.create_task(self._waiter())
1123-
raise # Pass cancellation to calling code
1124-
else:
1125-
await super().wait()
1126-
```
1127-
An instance may be set by a hard ISR or from another thread/core. As an `Event`
1128-
it can support multiple tasks and must explicitly be cleared.
1103+
See [Threadsafe Event](./THREADING.md#31-threadsafe-event) for a thread safe
1104+
class which allows multiple tasks to wait on it.
11291105

11301106
###### [Contents](./TUTORIAL.md#contents)
11311107

@@ -2883,3 +2859,17 @@ This, along with other issues, is discussed in
28832859
[Interfacing uasyncio to interrupts](./INTERRUPTS.md).
28842860

28852861
###### [Contents](./TUTORIAL.md#contents)
2862+
2863+
# 10. Interfacing threaded code
2864+
2865+
In the context of a `uasyncio` application, the `_thread` module has two main
2866+
uses:
2867+
1. Defining code to run on another core (currently restricted to RP2).
2868+
2. Handling blocking functions. The technique assigns the blocking function to
2869+
another thread. The `uasyncio` system continues to run, with a single task
2870+
paused pending the result of the blocking method.
2871+
2872+
These techniques, and thread-safe classes to enable their use, are presented in
2873+
[this doc](./THREAD.md).
2874+
2875+
###### [Contents](./TUTORIAL.md#contents)

0 commit comments

Comments
 (0)