-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Simpler mechanisms for asynchronous processing (thoughts) #1380
Comments
For a different and interesting approach to asynchronous processing, see @bboser's https://github.com/bboser/eventio for a highly constrained way of using |
I'm unqualified at this point to talk about implementation, but from an end user perspective I like the idea of this abstraction quite a bit. It feels both like a way to shortcut some ad hoc polling loop logic that I suspect people duplicate a lot (and often badly), and also something that could be relatively friendly to people who came up on high-level languages in other contexts. People aren't going to stop wanting interrupts / parallelism, but this answers a lot of practical use cases. |
I like event queues and I agree they are quite easy to understand and use, however, I'd like to point out a couple of down sides for them, so that we have more to discuss.
That's all I can think of at the moment. |
Hm, I do not fully understand, why such a MessageQueue model should be easier to understand than callbacks. Maybe it's, because I am used to callbacks ;-) I think, you have to invest much more brain in managing a couple of MessageQueues for different types of events (ethernet, i2c, timer, exceptions, spi, .....) or one MessageQueue, where you have to distinguish between different types of events, than in implementing one callback for each type of event ant pass it to a built in callback-handler.
|
I like the idea of message queues but I'm not convinced that they're any easier to understand than interrupt handers. Rather I think that conceptually interrupt handlers/callbacks are relatively easy to understand but understanding how to work with their constraints is where it gets a bit more challenging. Message queues are a good way of implementing the "get the operable data out of the hander and work on it in the main loop" solution to the constraints of interrupt handlers but as @deshipu pointed out, there are still good reasons to need to put some logic in the handler. Maybe both? Similarly I like how eventio works but I think it's even more confusing than understanding and learning to work with the constraints of interrupt handlers. That in mind, it's tackling concurrency in a way that I think might be more relatable to someone who came to concurrency from the "why can't I blink two leds at once" angle. One thing I was wondering about is what a bouncy button would do to a message queue. Ironically I think overflow might actually be somewhat useful in this case as if the queue was short enough you'd possibly lose the events for a number of bounces (but not all of them unless your queue was len=1. I'll have to ponder this one further). With a longer queue you could easily write a debouncer by looking for a time delta between events above a threshold. No matter how you slice it, concurrency is a step beyond the basics of programming and I don't think any particular approach is going to allow us to avoid that. It seems to me that we're being a bit focused choosing a solution to a set of requirements that we don't have a firm grasp on yet. I think it's worth taking the time to understand who the users of this solution are and what their requirements are. |
See #1415 for an async/await example. |
The problem is not with the callback mechanism itself, but in the constraint that MicroPython has that you can't allocate memory inside a callback. This is made much more complex than necessary by the fact that Python is a high level language with automatic memory management, that lets you forget about memory allocation most of the time, so it's not really obvious what operations can be used in a callback, and how to work around the ones that can't. |
One solution would be to enable Refs: |
In my implementation of interupts I’ve added a boolean “fast” that defaults
to false and controls running the handler via the scheduler (no constraints
on allocation) or directly in cases where latency is critical.
I am also considering running the gc automatically in the eventio loop but
have not yet considered all potential side effects. Ditto permitting
interrupt handlers is straightforward in eventio, for cases when they are
needed.
Bernhard
…On Sat, Dec 22, 2018 at 02:27 Noralf Trønnes ***@***.***> wrote:
One solution would be to enable MICROPY_ENABLE_SCHEDULER and *only* allow
soft IRQ's, running the callback inline with the VM. This would prevent
people from shooting themselves in the foot.
Refs:
- stm32:Handle_EXTI_Irq
<http://elixir.tronnes.org/circuitpython/4.0.0-alpha.4/source/ports/stm32/extint.c#L520>
- esp32:machine_timer_isr
<http://elixir.tronnes.org/circuitpython/4.0.0-alpha.4/source/ports/esp32/machine_timer.c#L99>
- py/vm.c
<http://elixir.tronnes.org/circuitpython/4.0.0-alpha.4/source/py/vm.c#L1297>
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#1380 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AC3bpFgD_0IZDU1HT4_V5XkegYEUsY5zks5u7YqOgaJpZM4ZFtp->
.
|
Thank you all for your thoughts and trials on this. I'll follow up in the near future but am deep in Bluetooth at the moment. The soft interrupts idea and the simplified event loop / async / await stuff is very interesting. I think we can make some progress on this. |
From my experience with what I think is the CircuitPython audience (I teach technology to designers), I don't think message queues are easier to understand than other approaches, and are probably harder in many cases. As @siddacious says, concurrency takes a while for newcomers to wrap their heads around no matter what the method. I also think it's important to distinguish between event driven needs and parallelism. In my experience, the most common need amongst my students is doing multiple things at once, e.g. fading two LEDs at different rates, and perhaps doing this while polling a distance sensor. This requirement is different from the straw man example above. Some possible directions:
|
I have done some more thinking and studying on this topic. In particular, I've read (well, read the front part; skimmed a lot more) Using Asyncio in Python 3, and I've read about curio and trio (github), which is even simpler than curio, started by @njsmith. Trio emphasizes "structured concurrency". I also re-reviewed @bboser 's https://github.com/bboser/eventio, and @notro 's example of a simple Also I had some thoughts about some very simple syntax for an event-loop system I thought might be called Maybe the functions below need to be Note that that I am pretty excited about this # A cute name
import when
import board, digitalio
d1 = digitalio.DigitalInOut(board.D1)
d1.switch_to_output()
d2 = digitalio.DigitalInOut(board.D2)
d2.switch_to_output()
d3_interrupt = digitalio.Interrupt(board.D3, change=digitalio.Interrupt.RISIING)
#################################
# Decorator style of using `when`
#Starts at 0.0 seconds, runs every 1.0 seconds
@when.interval(d1, interval=1.0)
def blink1(pin):
pin.value = not pin.value
# Starts at 0.5 seconds, runs every 1.0 seconds
@when.interval(d2, interval=1.0, start_at=0.5)
def blink2(pin):
pin.value = not pin.value
# This is a soft interrupt. The actual interrupt will set a flag or queue an event.
@when.interrupt(d3_interupt)
def d3_interrupt_handler(interrupt):
print("interrupted")
# Start an event loop with all the decorated functions above.
when.run()
####################################
# Programmatic style of using `when`
def toggle_d1():
d1.value = not d1.value
def toggle_d1():
d2.value = not d2.value
when.interval(toggle_d1, interval=1.0)
when.interval(toggle_d2, interval=1.0, start_at=0.5)
def d3_interrupt_handler():
print("interrupted")
when.interrupt(d3_interrupt_handler, d3_interupt)
when.run() |
I started a thread in the trio forum: https://trio.discourse.group/t/python-for-microcontrollers-and-structured-concurrency/154 |
For comparison, here is how you would do it with some kind of async framework (let's call it "suddenly"): import suddenly
import digitalio
async def blink1(pin):
pin.switch_to_output()
while True:
pin.value = not pin.value
await suddenly.sleep(1)
async def blink2(pin):
await suddenly.sleep(0.5)
await blink1(pin)
async def interrupt(pin):
while True:
await pin.change(digitalio.Interrupt.RISING)
print("interrupted")
suddenly.start(blink1(digitalio.DigitalInOut(board.D1)))
suddenly.start(blink2(digitalio.DigitalInOut(board.D2)))
suddenly.start(interrupt(digitalio.DigitalInOut(board.D3)))
suddenly.run() or a shorter: suddenly.run(
blink1(digitalio.DigitalInOut(board.D1)),
blink2(digitalio.DigitalInOut(board.D2)),
interrupt(digitalio.DigitalInOut(board.D3)),
) |
@deshipu Right, right, yes, we're talking about the same thing! I left out the trio and curio use import when
# similar defs as in previous comment ...
# ...
# I am deliberately leaving out the `async`s because I want to understand we actually need them and when we don't. How much can we hide in the library?
with when.loop() as loop:
loop.interval(blink1, 1.0)
loop.interval(blink2, 1.0, start_at=0.5)
loop.interrupt(some_function)
loop.event(event_handler, queue=some_event_queue)
loop.done_after(60.0) # stop loop after 60 seconds
loop.done(some_predicate_function)
# ^^ Runs the loop until done.
# in general:
# loop.something(function_name_or_lambda, args=(), more args if necessary) |
It's not possible to "hide" the async keyword in the library, because then you create a function that is being invoked when you "call" it. With async, "call" will simply produce an iterator object, which the library can then exhaust in its main loop, handling any Futures it gets from it along the way. I think that syntax makes a very big difference for beginners, and that the "callback" style that you propose is very difficult to grasp for people not used to it. With the async style syntax, you basically write each function as if it was the only function in your program (you can test it as the only function), and then add the async to it and await to all parts that block, and it just works. |
@deshipu Thank you for the enlightenment. I'll rework the examples with |
@deshipu But I am seeing trio use what you call the "callback" style: Do you have an example of an await/async library that uses your style? Trimmed example from link above: async def child1():
# ...
async def child2():
# ...
async def parent():
print("parent: started!")
async with trio.open_nursery() as nursery:
nursery.start_soon(child1)
nursery.start_soon(child2)
trio.run(parent) |
Here is a very simple implementatin of such an async framwork, that can only await on a sleep function: import time
TASKS = []
class Task:
def __init__(self, when, coro):
self.coro = coro
self.when = when
def sleep(seconds):
return [seconds]
def start(*awaitables, delay=0):
now = time.monotonic()
for awaitable in awaitables:
TASKS.append(Task(now + delay, awaitable))
def run(*awaitables):
start(*awaitables)
while TASKS:
now = time.monotonic()
for task in TASKS:
if now >= task.when:
try:
seconds = next(task.coro)
except StopIteration:
TASKS.remove(task)
else:
task.when = now + seconds
# async def test():
def test1():
for i in range(10):
print(i)
# await sleep(1)
yield from sleep(1)
def test2():
yield from sleep(0.5)
yield from test1()
run(test1(), test2()) |
This presentation explains the trampoline trick that it uses: |
As for examples, asyncio uses that style: https://docs.python.org/3/library/asyncio.html |
Of course in a proper implementation you would use a priority queue for tasks that are delayed, and a |
@deshipu, thank you for the response.
While "meanwhile" looks like it can handle simple counters, I don't see any way to control the threads once they've started. The primitives aren't there. From the sample program, it also looks like one timer needs to know details about the other timer? Or is that a mistake in the sample program?
I totally agree that they're hard to implement. But I'm convinced that you can create race conditions with ANY multi-tasking/threading setup. As for being counter-intuitive to program, that's true with ANY paradigm shift. (If you're used to apples, then passion fruit can be weird.) That's no reason to prevent their use. "Striving for excellence motivates... striving for perfection is demoralizing" -Harriet B. Braiker |
There are no threads. This is cooperative multiprocessing — the tasks suspend their execution and let other tasks run explicitly, by yielding the control back to the main loop. The tasks don't have to know about each other's details, I'm not sure what you mean here. I'm also not sure what kind of primitives you require. Maybe you could give me a simple example of the kind of a program you wanted to write with those counters, and I can show you how this can be done with that library?
No, there are setups that force your programs to be correct. I mean, obviously you can always create race conditions communicating with external systems, but that's unrelated to parallelization of your program — a completely single-threaded code can do that too. |
We don't have any immediate plans to add async. @dhalbert is currently working on |
Interesting related discussion here: https://forum.micropython.org/viewtopic.php?f=2&t=8429 |
Thanks Scott! |
I've been thinking quite a bit on this topic about the primitives that I DO want. When I write threads in normal python (NP) using the threading library, I essentially have two choices as how to create a thread:
#1 is fine when you have simple functions that can operate independently and might use a couple global variables. What you've provided sort of feels like #1. However, if you want your thread to manipulate a lot of state variables, you really want them encapsulated into a class. And that's where #2 comes into play. Then there's thread management. How do you get an individual thread to change what it's doing? Or pass data to a thread? How does a thread end? How does it get removed from the active thread list? Using global variables (with #1) is fine when you want ALL of the threads using a particular function to change in the same way. However, finer grained management requires #2. Then there's cooperative passing of data between threads: consumers and producers. Programming those requires some sort of semaphore or locking mechanism. How do you encapsulate external events? Those are the types of primitives that I would like to be able to work with. So to summarize what I would want in a co-operative multi-processing library: a) easy way to say "run this function"
We'll have to agree to disagree. :) |
Just use any variable or attribute. You don't need special "safe" data structures, since control switches only happen in designated places, so all data structures are safe.
Can you elaborate on that? |
Is there any progress on this behind the scenes? I would also really like to see this happen in CPY. I think especially with the ESP32-S2 coming along you will absolutely need this for networking. |
@PTS93 @WarriorOfWire has discussed this with us extensively on discord, and created a simple library: https://github.com/WarriorOfWire/tasko. I intend to look at this in more detail fairly soon. I have been working on another BLE implementation and have not had time to work on this for the past few months. |
That looks good! Though I assume it will support more than just intervals? |
I don't represent Adafruit. I'm just a big fan. But it is my understanding
that CircuitPython will never be targeted to the ESP-32 because it doesn't
support host mountable USB disk. It was originally targeted but was dropped
when USB disk became the standard development tool. Adafruit considers that
to be a technology requirement for CircuitPython.
Adafruit has often pointed out that MicroPython is supported on ESP-32 and
various multiprocessing techniques have been implemented there.
CircuitPython is after all a fork of MicroPython so that is a Python
solution. What it lacks is the complete and tested support for all the
hundreads of Adafruit sensors and add-ons
…On Sat, Aug 22, 2020 at 2:22 PM Timon ***@***.***> wrote:
Is there any progress on this behind the scenes? I would also really like
to see this happen in CPY. I think especially with the ESP32-S2 coming
along you will absolutely need this for networking.
In my experience working with a lot of art students at my day job over the
years, events are the easiest concept to understand as it is just a
function thats being "called for you" but the asyncio way seems also fine
to me albeit it needing a bit more things to understand and take care of
but especially if the overall Python community is adopting this style it is
probably a good idea to stick with it in my opinion because many will come
from Python and may have already learned it.
—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
<#1380 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAJKESRXM6RRO7LIHV2XCG3SCAEHDANCNFSM4GIW3J7A>
.
|
@zencuke we are working on an ESP32S2 port. That does support USB. |
Good news. Thanks. I didn't know about the ESP-32 USB disk.
|
Hi, I am sorry but do not want to read all comments on this issue. I study and work on real-time systems. Circuit python is for microcontrollers. They are not PCs - real PC-multitasking on a single CPU is not needed at all. Everything that CircuitPython programmer needs is scheduler :) If three tasks (A, B, C) with two sections (example: AA) runs this way: In real-time applications, what is needed are priorities, a way to set order or schedule, and a tool that tells you when tasks run.
If someone implements scheduling framework into CircuitPython, someone else can implement its scheduler, and an inexperienced user can use it. There are multiple scheduling strategies. Every single one is good for a different problem, and no single strategy is right for everything. Multitasking implemented with the scheduler is the way to go. It is predictive and straightforward. If every task informs CircuitPython about its start and end, tasks can easily be visualized for an inexperienced user to debug deadlines, schedules, or priorities easily. I would be pleased if I found an EDF scheduler in CircuitPython or at least a way to set up static scheduling. I don't think it's going to be possible to implement advanced scheduler only in Python, and it's probably going to take help from inside of CircuitPython implementation. |
@eLEcTRiCZiTy that is exactly what async/await does. |
@deshipu Async&await is for PCs or Mobile phones when you do not need information on how asynchronous functions are executed in the background and only required is the result and or smooth GUI response. A small delay or more significant overhead is easily overlooked and usually does not bother anyone. ...and!.. usually good async scheduler needs another thread or some level of cooperative multitasking. When the async&await mechanism is implemented using some sort of synchronous polling it is not asynchronous, but it is a weird synchronous scheduler with wrong syntax sugar. So, the short answer: Long one: On microcontrollers, you need to manage tasks in an entirely different way than multithreading or async calling (asynchronous functions besides interrupts exist even in realtime systems, but they are much less common and they are scheduled differently). Task have time when it can start, and time when it must end. You need to control when the task is executed by yourself (static scheduling) or need a predictive scheduling algorithm because everything has side effects. Side effects are there intended or hidden. Intended are relay switching, led blinking, etc. Hidden side effects like memory allocation can be dangerous. Tasks or async routines have memory usage, bus usage, and peripheries can be power-hungry, etc. |
Hi @eLEcTRiCZiTy, this issue has moved well past the theoretical and into practical territory. Please feel free to avail yourself of the concrete code listings in this issue and both the Circuitpython and upstream Micropython projects if you'd like to share insights from a position of context and understanding. |
In any case, Please don't conflate these issues. |
@smurfix Hi, you are right. It is my fault. The scheduler is different problem and it is actually separable from an asynchronous calling implementation. Scheduler must support asynchronous tasks or they can be executed in a specific periodic task. |
async/await keyword support has been accepted into CircuitPython as of #3540 There is currently no native scheduler implementation or interrupt-scheduled coroutines (yet!) but as @dhalbert mentioned back in August simple time-based pure-python async scheduling has been demonstrated on CircuitPython; also it's easy to reproduce from scratch (and completely understand) with not much more than following along with the good Mister Beazley https://www.youtube.com/watch?v=Y4Gt3Xjd7G8 I think it's wonderful that async/await keywords are not tied to a specific implementation of concurrency - you can use a global event loop if you like, or you can keep track of all your tasks and call |
There's one missing piece here: we need to be able to schedule a task when something happens, i.e. an interrupt routine must be able to wake up the core scheduler. This requires an atomic "check whether this attribute of that object is MicroPython has |
yes, i mentioned that interrupt support is not there. But no, it is not necessary. If nobody can interrupt and there are no currently active tasks, then nobody can make a new task until the next scheduled task. See the code in the tasko library if you are unsure of how that works. Just like in CPython, you need to call sleep on the event loop if you want the event loop to do the sleeping. |
I'm not talking about the application, I'm talking about the event loop itself. In CPython the event loop doesn't call "sleep", it calls "select" or "epoll" or whatever, which is a sleep that can be stopped by what's essentially an interrupt. What do you mean, it's not necessary? Of course it's necessary. People want to put the MCU into some sleep state when it has nothing to do, that saves a ton of battery power. There are lots of situations where you want the sleep to end when an interrupt arrives. Input pin change, serial character arrives, I2C slave gets selected … why should I wake up my MCU every 10ms to check for that? that' what interrupts are for. I want to use them. An interruptible scheduler works by
Without steps 2 and 3 you'd get a race condition. You'd also get one if you re-enable interrupts before SLEEPing, the MCU must do that atomically as part of its SLEEP instruction. If To build a "real" event loop, we'd need to be able to call |
friend be my guest and implement it. I look forward to your pull request. I'm not sure if I'm communicating badly but literally all I'm trying to say is that async/await keywords exist and they do sane things (sane as defined by their CPython behavior). Scheduling is an utterly different problem altogether and yeah, if you are designing a scheduler of course you'd want interrupts but no you definitely do not need them to schedule tasks in an ecosystem that does not have interrupts. I've provided an existence proof. If you want them, or if you need them for some reason, please feel welcome to contribute. I also want them and will add support at some point when I have free time unless someone else has free time and motivation first. In the meantime, barebones async/await noun/verb pair should work great for people to learn on circuitpython, library support notwithstanding. |
Will do. |
These are some strawman thoughts about how to provide handling of asynchronous events in a simple way in CircuitPython. This was also discussed at some length in our weekly audio chat on Nov 12, 2018, starting at 1:05:36: https://youtu.be/FPqeLzMAFvA?t=3936.
Every time I look at the existing solutions I despair:
I don't think any of these are simple enough to expose to our target customers.
But I think there's a higher-level mechanism that would suit our needs and could be easily comprehensible to most users, and that's
Message Queues
A message queue is just a sequence of objects, usually first-in-first-out. (There could be fancier variations, like priority queues.)
When an asynchronous event happens, the event handler (written in C) adds a message to a message queue when. The Python main program, which could be an event loop, processes these as it has time. It can check one or more queues for new messages, and pop messages off to process them. NO Python code ever runs asynchronously.
Examples:
When you want to process asynchronous events from some builtin object, you attach it to a message queue. That's all you have to do.
There are even already some Queue classes in regular Python that could serve as models: https://docs.python.org/3/library/queue.html
Some example strawman code is below. The method names are descriptive -- we'd have to do more thinking about the API and its names.
Or, for network packets:
For UART input:
Unpleasant details about queues and storage allocation:
It would be great if queues could just be potentially unbounded queues of arbitrary objects. But right now the MicroPython heap allocator is not re-entrant, so an interrupt handler or packet receiver, or some other async thing can't allocate the object it want to push on the queue. (That's why MicroPython has those restrictions on interrupt handlers.) The way around that is pre-allocate the queue storage, which also makes it bounded. Making it bounded also prevents queue overflow: if too many events happen before they're processed, events just get dropped (say either oldest or newest). So the queue creation would really be something like:
The whole idea here is that event processing takes place synchronously, in regular Python code, probably in some kind of event loop. But the queues take care of a lot of the event-loop bookkeeping.
If and when we have some kind of multiprocessing (threads or whatever), then we can have multiple event loops.
The text was updated successfully, but these errors were encountered: