Skip to content

Commit f7c44fa

Browse files
committed
Move Message class and docs to threadsafe.
1 parent 4dfecdd commit f7c44fa

File tree

6 files changed

+178
-98
lines changed

6 files changed

+178
-98
lines changed

v3/docs/THREADING.md

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ It is not an introduction into ISR coding. For this see
1111
and [this doc](https://github.com/peterhinch/micropython-async/blob/master/v3/docs/INTERRUPTS.md)
1212
which provides specific guidance on interfacing `uasyncio` with ISR's.
1313

14+
Because of [this issue](https://github.com/micropython/micropython/issues/7965)
15+
the `ThreadSafeFlag` class does not work under the Unix build. The classes
16+
presented here depend on this: none can be expected to work on Unix until this
17+
is fixed.
18+
1419
# Contents
1520

1621
1. [Introduction](./THREADING.md#1-introduction) The various types of pre-emptive code.
@@ -25,6 +30,8 @@ which provides specific guidance on interfacing `uasyncio` with ISR's.
2530
     2.2.3 [Object ownership](./THREADING.md#223-object-ownership)
2631
3. [Synchronisation](./THREADING.md#3-synchronisation)
2732
3.1 [Threadsafe Event](./THREADING.md#31-threadsafe-event)
33+
3.2 [Message](./THREADING.md#32-message) A threadsafe event with data payload.
34+
4. [Taming blocking functions](./THREADING.md#4-taming-blocking-functions)
2835

2936
# 1. Introduction
3037

@@ -376,3 +383,165 @@ async def main():
376383

377384
asyncio.run(main())
378385
```
386+
## 3.2 Message
387+
388+
The `Message` class uses [ThreadSafeFlag](./TUTORIAL.md#36-threadsafeflag) to
389+
provide an object similar to `Event` with the following differences:
390+
391+
* `.set()` has an optional data payload.
392+
* `.set()` can be called from another thread, another core, or from an ISR.
393+
* It is an awaitable class.
394+
* Payloads may be retrieved in an asynchronous iterator.
395+
* Multiple tasks can wait on a single `Message` instance.
396+
397+
Constructor:
398+
* No args.
399+
400+
Synchronous methods:
401+
* `set(data=None)` Trigger the `Message` with optional payload (may be any
402+
Python object).
403+
* `is_set()` Returns `True` if the `Message` is set, `False` if `.clear()` has
404+
been issued.
405+
* `clear()` Clears the triggered status. At least one task waiting on the
406+
message should issue `clear()`.
407+
* `value()` Return the payload.
408+
409+
Asynchronous Method:
410+
* `wait()` Pause until message is triggered. You can also `await` the message
411+
as per the examples.
412+
413+
The `.set()` method can accept an optional data value of any type. The task
414+
waiting on the `Message` can retrieve it by means of `.value()` or by awaiting
415+
the `Message` as below. A `Message` can provide a means of communication from
416+
an interrupt handler and a task. The handler services the hardware and issues
417+
`.set()` which causes the waiting task to resume (in relatively slow time).
418+
419+
This illustrates basic usage:
420+
```python
421+
import uasyncio as asyncio
422+
from threadsafe import Message
423+
424+
async def waiter(msg):
425+
print('Waiting for message')
426+
res = await msg
427+
print('waiter got', res)
428+
msg.clear()
429+
430+
async def main():
431+
msg = Message()
432+
asyncio.create_task(waiter(msg))
433+
await asyncio.sleep(1)
434+
msg.set('Hello') # Optional arg
435+
await asyncio.sleep(1)
436+
437+
asyncio.run(main())
438+
```
439+
The following example shows multiple tasks awaiting a `Message`.
440+
```python
441+
from threadsafe import Message
442+
import uasyncio as asyncio
443+
444+
async def bar(msg, n):
445+
while True:
446+
res = await msg
447+
msg.clear()
448+
print(n, res)
449+
# Pause until other coros waiting on msg have run and before again
450+
# awaiting a message.
451+
await asyncio.sleep_ms(0)
452+
453+
async def main():
454+
msg = Message()
455+
for n in range(5):
456+
asyncio.create_task(bar(msg, n))
457+
k = 0
458+
while True:
459+
k += 1
460+
await asyncio.sleep_ms(1000)
461+
msg.set('Hello {}'.format(k))
462+
463+
asyncio.run(main())
464+
```
465+
Receiving messages in an asynchronous iterator:
466+
```python
467+
import uasyncio as asyncio
468+
from threadsafe import Message
469+
470+
async def waiter(msg):
471+
async for text in msg:
472+
print(f"Waiter got {text}")
473+
msg.clear()
474+
475+
async def main():
476+
msg = Message()
477+
task = asyncio.create_task(waiter(msg))
478+
for text in ("Hello", "This is a", "message", "goodbye"):
479+
msg.set(text)
480+
await asyncio.sleep(1)
481+
task.cancel()
482+
await asyncio.sleep(1)
483+
print("Done")
484+
485+
asyncio.run(main())
486+
```
487+
The `Message` class does not have a queue: if the instance is set, then set
488+
again before it is accessed, the first data item will be lost.
489+
490+
# 4. Taming blocking functions
491+
492+
Blocking functions or methods have the potential of stalling the `uasyncio`
493+
scheduler. Short of rewriting them to work properly the only way to tame them
494+
is to run them in another thread. The following is a way to achieve this.
495+
```python
496+
async def unblock(func, *args, **kwargs):
497+
def wrap(func, message, args, kwargs):
498+
message.set(func(*args, **kwargs)) # Run the blocking function.
499+
msg = Message()
500+
_thread.start_new_thread(wrap, (func, msg, args, kwargs))
501+
return await msg
502+
```
503+
Given a blocking function `blocking` taking two positional and two keyword args
504+
it may be awaited in a `uasyncio` task with
505+
```python
506+
res = await unblock(blocking, 1, 2, c = 3, d = 4)
507+
```
508+
The function runs "in the background" with other tasks running; only the
509+
calling task is paused. Note how the args are passed. There is a "gotcha" which
510+
is cancellation. It is not valid to cancel the `unblock` task because the
511+
underlying thread will still be running. There is no general solution to this.
512+
If the specific blocking function has a means of interrupting it or of forcing
513+
a timeout then it may be possible to code a solution.
514+
515+
The following is a complete example where blocking is demonstrated with
516+
`time.sleep`.
517+
```python
518+
import uasyncio as asyncio
519+
from threadsafe import Message
520+
import _thread
521+
from time import sleep
522+
523+
def slow_add(a, b, *, c, d): # Blocking function.
524+
sleep(5)
525+
return a + b + c + d
526+
527+
# Convert a blocking function to a nonblocking one using threading.
528+
async def unblock(func, *args, **kwargs):
529+
def wrap(func, message, args, kwargs):
530+
message.set(func(*args, **kwargs)) # Run the blocking function.
531+
msg = Message()
532+
_thread.start_new_thread(wrap, (func, msg, args, kwargs))
533+
return await msg
534+
535+
async def busywork(): # Prove uasyncio is running.
536+
while True:
537+
print("#", end="")
538+
await asyncio.sleep_ms(200)
539+
540+
async def main():
541+
bw = asyncio.create_task(busywork())
542+
res = await unblock(slow_add, 1, 2, c = 3, d = 4)
543+
bw.cancel()
544+
print(f"\nDone. Result = {res}")
545+
546+
asyncio.run(main())
547+
```

v3/docs/TUTORIAL.md

Lines changed: 7 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1343,106 +1343,17 @@ finally:
13431343

13441344
## 3.9 Message
13451345

1346-
Because of [this issue](https://github.com/micropython/micropython/issues/7965)
1347-
the `Message` class does not work under the Unix build.
1346+
The `Message` class uses [ThreadSafeFlag](./TUTORIAL.md#36-threadsafeflag) to
1347+
provide an object similar to `Event` with the following differences:
13481348

1349-
This is an unofficial primitive with no counterpart in CPython asyncio. It uses
1350-
[ThreadSafeFlag](./TUTORIAL.md#36-threadsafeflag) to provide an object similar
1351-
to `Event` but capable of being set in a hard ISR context. It extends
1352-
`ThreadSafeFlag` so that multiple tasks can wait on an ISR.
1353-
1354-
It is similar to the `Event` class. It differs in that:
13551349
* `.set()` has an optional data payload.
1356-
* `.set()` is capable of being called from a hard or soft interrupt service
1357-
routine.
1350+
* `.set()` can be called from another thread, another core, or from an ISR.
13581351
* It is an awaitable class.
1359-
* It can be used in an asynchronous iterator.
1360-
* The logic of `.clear` differs: it must be called by at least one task which
1361-
waits on the `Message`.
1362-
1363-
The `.set()` method can accept an optional data value of any type. The task
1364-
waiting on the `Message` can retrieve it by means of `.value()` or by awaiting
1365-
the `Message` as below.
1366-
1367-
Like `Event`, `Message` provides a way for a task to pause until another flags it
1368-
to continue. A `Message` object is instantiated and made accessible to the task
1369-
using it:
1370-
1371-
```python
1372-
import uasyncio as asyncio
1373-
from primitives import Message
1374-
1375-
async def waiter(msg):
1376-
print('Waiting for message')
1377-
res = await msg
1378-
print('waiter got', res)
1379-
msg.clear()
1380-
1381-
async def main():
1382-
msg = Message()
1383-
asyncio.create_task(waiter(msg))
1384-
await asyncio.sleep(1)
1385-
msg.set('Hello') # Optional arg
1386-
await asyncio.sleep(1)
1387-
1388-
asyncio.run(main())
1389-
```
1390-
A `Message` can provide a means of communication between an interrupt handler
1391-
and a task. The handler services the hardware and issues `.set()` which causes
1392-
the waiting task to resume (in relatively slow time).
1393-
1394-
Constructor:
1395-
* No args.
1352+
* Payloads may be retrieved in an asynchronous iterator.
1353+
* Multiple tasks can wait on a single `Message` instance.
13961354

1397-
Synchronous methods:
1398-
* `set(data=None)` Trigger the `Message` with optional payload (may be any
1399-
Python object).
1400-
* `is_set()` Returns `True` if the `Message` is set, `False` if `.clear()` has
1401-
been issued.
1402-
* `clear()` Clears the triggered status. At least one task waiting on the
1403-
message should issue `clear()`.
1404-
* `value()` Return the payload.
1405-
1406-
Asynchronous Method:
1407-
* `wait()` Pause until message is triggered. You can also `await` the message
1408-
as per the examples.
1409-
1410-
The following example shows multiple tasks awaiting a `Message`.
1411-
```python
1412-
from primitives import Message
1413-
import uasyncio as asyncio
1414-
1415-
async def bar(msg, n):
1416-
while True:
1417-
res = await msg
1418-
msg.clear()
1419-
print(n, res)
1420-
# Pause until other coros waiting on msg have run and before again
1421-
# awaiting a message.
1422-
await asyncio.sleep_ms(0)
1423-
1424-
async def main():
1425-
msg = Message()
1426-
for n in range(5):
1427-
asyncio.create_task(bar(msg, n))
1428-
k = 0
1429-
while True:
1430-
k += 1
1431-
await asyncio.sleep_ms(1000)
1432-
msg.set('Hello {}'.format(k))
1433-
1434-
asyncio.run(main())
1435-
```
1436-
Receiving messages in an asynchronous iterator:
1437-
```python
1438-
msg = Message()
1439-
asyncio.create_task(send_data(msg))
1440-
async for data in msg:
1441-
# process data
1442-
msg.clear()
1443-
```
1444-
The `Message` class does not have a queue: if the instance is set, then set
1445-
again before it is accessed, the first data item will be lost.
1355+
It may be found in the `threadsafe` directory and is documented
1356+
[here](./THREADING.md#32-message).
14461357

14471358
## 3.10 Synchronising to hardware
14481359

v3/primitives/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ def _handle_exception(loop, context):
3636
"Condition": "condition",
3737
"Delay_ms": "delay_ms",
3838
"Encode": "encoder_async",
39-
"Message": "message",
4039
"Pushbutton": "pushbutton",
4140
"ESP32Touch": "pushbutton",
4241
"Queue": "queue",

v3/primitives/tests/asyntest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
from primitives import Barrier, Semaphore, BoundedSemaphore, Condition, Queue, RingbufQueue
2020
try:
21-
from primitives import Message
21+
from threadsafe import Message
2222
except:
2323
pass
2424

v3/threadsafe/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
_attrs = {
1212
"ThreadSafeEvent": "threadsafe_event",
1313
"ThreadSafeQueue": "threadsafe_queue",
14+
"Message": "message",
1415
}
1516

1617
# Copied from uasyncio.__init__.py
File renamed without changes.

0 commit comments

Comments
 (0)