-
Notifications
You must be signed in to change notification settings - Fork 13.3k
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
add regular scheduled functions, now also callable on yield()
#6039
Conversation
added bool schedule_function_us(std::function<bool(void)> fn, uint32_t repeat_us) lambda must return true to be not removed from the schedule function list if repeat_us is 0, then the function is called only once. Legacy schedule_function() is preserved Linked list management is simplified This addition allows network drivers like ethernet chips on lwIP to be regularly called - even if some user code loops on receiving data without getting out from main loop (callable from yield()) - without the need to call the driver handling function (transparent) This may be also applicable with common libraries (mDNS, Webserver, )
This is *dependant* on this esp8266 arduino core pull request: esp8266/Arduino#6039 Fix #3 (comment)
This may be more of a scheduled_functions general question, but what about interrupt safety? One use I see for this is to put a notice into a queue in an IRQ (instead of actually doing work there). But you could also have things like repeated ones, now, in the main app. So isn't there a race condition in the list update stage (or if, say, an IRQ happens while parsing through the existing list)? |
b58f12a
to
b6564c2
Compare
You are right. edit: still not sure this is the right way
|
That looks good and seems like it will preserve the last IRQ level which is what is needed. I think you also need locking around |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
May I bring the existence of cores/esp8266/interrupts.h to your attention? That actually gets included in Esp.cpp already.
About the use of volatile, I have some reserverations - IIRC, memory fences must be handled differently.
I've grepped across the source tree, and interrupts are disabled in one form or the other in any of these places: libraries/EEPROM/EEPROM.cpp: noInterrupts(); I gather, that timer and GPIO interrupt handlers by design decision ("AVR compatibility" gets mentioned) always run with interrupts disabled, reducing the changes that an ISR gets interrupted to about zero. I think tools/sdk/lwip2/builder/glue-esp/lwip-esp.c maintains a linked list a lot like the one that's being discussed here, and stands out as having a rather large block of code during which interrupts are disabled. Now, late at night, all I can contribute toward a solution is this article, containing supposedly lock-free code for a queue: I will need to look into that for EspSoftwareSerial, the ISR buffer interacts with user code and has all the possibility of issues that I suggest this PR might have. |
Sure, and thanks for it. I wasn't aware (!)
This volatile is unnecessary, I forgot to remove it.
Sure but they are necessary to avoid races.
Registers used in a function (or all registers) are pushed on the stack and restored before the "reti". |
That's what I believe and that's the reason to use std::atomic load and store - otherwise all interrupt locking may not be much help. Or am I completely missing something? |
@dok-net, what specifically are you worried about? The SDK blob IRQ wrapper stores all the registers in use before calling an IRQ function and then restores then before returning from interrupt. I don't think there's any particular concern there, it's a simple and common operation. On function entry in main code you call IRQ-disable as the first state changing operation. That either finishes uninterrupted, or an IRQ gets called. That IRQ may call the same function, (and can't be interrupted) so will run to completion changing the linked list). On return from interrupt the main app's locking code completes and it has full access to the updated list (there is no chance of the list being cached incoherently anywhere in the machine) and can do its own work... |
@d-a-v @earlephilhower Is there a quick explanation why linked lists are used? IIRC linked lists are discouraged for use where lock-free programming is an advantage. I just too a perfunctory glance at the code spots and couldn't figure out why linked links are used - if all the reason should be to keep entries in for repeated execution, pop and re-push should do the trick, too. I've implemented a lock-free (well, on ESP32, on single-threaded ESP8266 it disable IRQs after all ;-) ) ring buffer / circular queue for EspSoftwareSerial, which I am still testing. It's multi-producer, single-consumer capable, and could be a nice basis to #6039... |
@dok-net I didn't do any of the coding on this, but I imagine linked lists were used to minimize heap usage when only a few (or common case: 0) delayed functions are in play. There's no atomic TAS operations on the chip, though, so even simple things like mutexes aren't doable safely w/o stopping IRQs. I would not want to guarantee that std::atomic (if available, I don't remember if it compiled or not) works as-expected with interrupts on this chip, actually. In any case, I'm still trying to understand the concern here. Logically, I think it's fine. Are you concerned about performance (i.e. you have a use where you have to add items to the queue at high frequency, etc.? W/only 32 slots even that's a pretty bounded problem) or something else? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Due to multiple changes, I've left a PR with explanations - d-a-v#6
{ | ||
return schedule_function_us([&fn](){ fn(); return false; }, 0); | ||
} | ||
|
||
void run_scheduled_functions() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMHO: This linked-list implementation is not - probably never was - preemption safe, generally a compiler will keep the values of all the pointers in registers, even on the single-core ESP8266 an IRQ will not flush the registers but just push them to the stack and restore them, therefore any IRQ that's scheduling functions fails during an ongoing run_scheduled_functions(). Blocking IRQs during the complete execution of run_scheduled_functions makes it thread/IRQ safe, but I don't think this is permissible from an IRQ performance POV.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point.
We don't want to lock while executing the scheduled function themselvses.
One solution is to tag variables with volatile
.
Another one is to build a local copy of the list while being locked, then unlock and run that list.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I must step back on this.
Locking IRQ is the right thing to do when the value of a variable can be modified in a TAS block.
Even if there are registers holding some variables, they will not be changed while in a locked block because an IRQ won't occur in that block, regardless whether the variable is cached in a register.
I think the compiler will not / must not optimize a variable into registers when is it not declared locally.
@d-a-v What happens if a scheduled function/task calls yield() etc. itself? Calling https://www.arduino.cc/en/Reference/SchedulerStartLoop |
My thinking is these scheduled functions should be thought as interrupt functions (that are artificially shifted out from sys stack). If it happens we need to be calling other functions that themselves call yield, then we can add a boolean fence set up in
That can be something we think about when we will move from nonos-sdk to rtos-sdk which will happen sometimes soon enough.
check |
@d-a-v please revisit my edited comment above regarding https://github.com/arduino-libraries/Scheduler/ |
(edited: task->stack) These schedulers are multitasking schedulers with each its own task. |
Which is it ? |
Null pointer access in last line of get_fn_unsafe |
Proposed changes from review
6b95675
to
8e06c30
Compare
@d-a-v Big oops, something we've all missed:
lastRecurring should get updated
|
@d-a-v Could we maybe agree to rename toCall as next? I've really a deep expectation in my mind that toCall is the current, to call, item, and it's making the piece of code very hard to reason about. |
In that case: |
Already updated in #6137 |
added
bool schedule_function_us(std::function<bool(void)> fn, uint32_t repeat_us)
lambda must return
true
to be not removed from the schedule function listif
repeat_us
is 0, then the function is called only once.Legacy
schedule_function()
is preservedLinked list management is simplifiedThis addition allows network drivers like ethernet chips on lwIP to be regularly called
(callable from
yield()
)(transparent)
This may be also applicable with common libraries (mDNS, Webserver, )