-
Notifications
You must be signed in to change notification settings - Fork 3k
Event loop #2801
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
Event loop #2801
Conversation
This commit introduces another Mail class, called DynamicMail. The only difference between them is that DynamicMail allocates memory dynamically for the mail-related data structures, while Mail keeps the data structures as part of the instance itself (like it did before). The common implementation of the two classes was factored out into a generic implementation class (GenericMail) that can't be instantiated directly (since it doesn't know anything about the mail-related data structures storage). Also updated the Mail test to test both a Mail and a DynamicMail instance.
38902de
to
de9b495
Compare
Event is a callback that executes either immediately, like a regular callback, or is posted in an event queue for defered execution. EventLoop is a queue of events, built on top of the DynamicMail object. It runs in its own thread and dispatches events as they arrive in the mail.
In the core classes, the callback event type (used for 'attach'-like functions) was 'Callback<void()>'. This commit changes the type to Event, which allows the callbacks to be used both like regular callbacks, or enqueued in an event queue for deferred execution. The commit preserves backward compatibility (the default behaviour is still to run the callbacks immediately (like 'Callback' does), not defer them). Event and Callback have similar constructors and attach() functions, so it was enough to replace one type with the other, without other code changes. Future similar changes to the API should be able to follow the same pattern.
* executed immediately (EVENT_CALL_IMMEDIATELY) or enqueued in the | ||
* system queue (EVENT_CALL_ENQUEUE) | ||
*/ | ||
void call() const; |
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.
Should we also provide an operator()
member function for for the Event?
This may be feature creep, but a thunk
member function would be useful for C callback APIs:
https://github.com/ARMmbed/mbed-os/blob/master/hal/api/Callback.h#L440
For example:
lwip_network_callback(void (*cb)(void *), void *data);
Event ev;
lwip_network_callback(&Event::thunk, &ev);
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.
Just missed that you took care of this already 👍
* @param queue Pointer to the event queue (NULL to call the event immediately) | ||
* @return Callback with infered type | ||
*/ | ||
Event event(void (*func)(void) = 0, EventQueue *queue = 0); |
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.
If we allow a null queue here, we've locked ourselves out into ticker.attach(event(doit))
being in irq context.
Currently irq context is possible (and backwards compatible) via the Event
overloads:
ticker.attach(callback(doit));
// or just
ticker.attach(doit);
It we don't allow a null queue for irq context, this opens up the option of slipping in a system queue in the future:
ticker.attach(callback(doit)); // irq context
ticker.attach(event(doit)); // user context on system queue
ticker.attach(event(doit, &queue)); // user context on user provided queue
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.
Nice taking care of this already 👍
|
||
/** Starts the event queue thread | ||
*/ | ||
void start(); |
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'm not sure about this composition of an EventQueue
and Thread
. Although marginally more convenient for user created event queues, providing a public dispatch
function would be sufficient:
EventQueue queue(32);
Thread thread(2048);
thread.start(&queue, &EventQueue::dispatch);
This would additionally allow the user to configure the Thread object and take advantage of set_priority
, signal_set
, and other thread functions, as well as pass the specific thread around to function built on the thread type.
Additionally, a public dispatch function would be more flexible, and resolve the issues with rtos dependencies.
If we aren't planning on exposing a public dispatch member, I would push to rename this EventQueue
to EventLoop
or something similar, to give us the option of introducing a thread-less EventQueue
base class in the future.
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.
Nice taking care of this already 👍
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.
In the current implementation, the event queue doesn't actually work without a Thread. In that sense, I believe the composition is reasonable, since it makes this contract explicit. We could start it separately, yes, but I personally think that the added flexibility adds to the level of confusion of the class user.
* @param queue Pointer to the event queue | ||
* @return Callback with infered type | ||
*/ | ||
Event event(void (*func)(void), EventLoop *queue); |
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.
This may be subjective, but we could provide these convenience functions as member functions on the EventLoop
class:
ticker.attach(queue->event(func));
// vs
ticker.attach(event(func, &queue));
This avoids ambiguity on if queue
effects the event or is just the argument to func
.
@mbed-bot: TEST HOST_OSES=ALL |
/morph test |
Signing out now, will resume looking at this tomorrow. |
Result: FAILUREYour command has finished executing! Here's what you wrote!
Outputmbed Build Number: null Examples Build failed! |
[Build 975] |
[Build ${MBED_BUILD_ID}] |
} | ||
|
||
void EventLoop::start() { | ||
_thread.start(callback(this, &EventLoop::dispatch)); |
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.
Should this propogate errors from the thread start function?
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 will change this, thanks.
} | ||
|
||
void EventLoop::stop() { | ||
_thread.terminate(); |
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.
Wouldn't _thread.join()
be a better way to stop the event queue? This would work well if you had a flag in the actual dispatch loop.
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.
Why would it be better? Is terminate
problematic? I couldn't find any references to this in the RTX docs, or our docs.
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.
@bogdanm Unlike terminate
, join
can allows the user to do a proper cleanup.
Maybe the behavior can be selected with an input parameter:
TERMINATE
: terminate right away, current activity is stopped; might lead to resources leak.STOP_AFTER_CURRENT_EVENT
: Stop the event loop once the current event has been processed. The event loop doesn't accept events after this point.STOP_WHEN_QUEUE_EMPTY
: The event loop doesn't accept events after this point. Stop when the queue is empty.REQUEST_STOP
: The event loop accept events after this point. Stop when the queue is empty. This might never stop
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 see what you mean, thanks. It seems a bit overkill to provide all those termination modes. It looks like the most useful one is STOP_WHEN_QUEUE_EMPTY
(and possibly TERMINATE
for immediate and unconditional termination, but even that doesn't feel right after your explanation above).
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 added a commit that implements this behaviour (empty the event queue before stopping), please let me know what you think.
@bogdanm I'm a bit confused about this PR. While it sounds reasonable for the HAL APIs, I don't see myself use it for the following reasons:
Another issue with this model is the memory wasted in the process: int main() {
queue.start();
sw.rise(event(handler, &queue));
Thread::wait(osWaitForever);
} In that scenario, the stack of the main thread will never be used. On a platforms like the NRF51_DK, it is
void bar();
void foo(EventLoop* el) {
// Invalid, timeout will be destroyed at the end of the function!
Timeout timeout;
timeout.attach(event(bar, el), 1000.0);
} One solution would be to use a global variable but this doesn't scale well in term of memory usage and it is difficult to use: One global variable have to be declared every time the user code needs a recurring event or a deferred event and the programmer has to makes sure that it is wrapped inside a
class Foo {
process_bar(int a , int b);
}
Foo foo;
EventLoop evl;
//////////////////////////////////////////////////////////////////////
// Current version:
class BarEvent {
friend Callback<void()> make_BarEvent(int a, int b, Foo& f);
bar_event(int a, int b, Foo& f) :
_a(a), _b(b), _f(f) {
}
void execute() {
f.process_bar(_a, _f);
delete this;
}
int _a;
int _b;
Foo& _f;
}
Callback<void()> make_BarEvent(int a, int b, Foo& f) {
bar_event* ev = new BarEvent(a, b, f);
return Callback<void()>(ev, &BarEvent::execute);
}
void bar() {
int a = get_a();
int b = get_b();
evl.post(make_BarEvent(a, b, foo));
} with a binding facility, this code would be much more simpler: void bar() {
int a = get_a();
int b = get_b();
evl.post(bind(&Foo::process_bar, &foo, a, b));
} The // equivalent to the `event` function
Callback<void()> defer(const Callback<void()>& cb, EventLoop& event_loop) {
return bind(&EventLoop::post, event_loop, cb);
} With such facility, the intention become more explicit in my opinion (execute directly or defer to an event loop): EventLoop event_loop;
InterruptIn foo;
void handle_foo_rise();
// direct call
foo.rise(handle_foo_rise);
// deferred via event_loop
foo.rise(defer(handle_foo_rise, event_loop)); |
Hi @pan-. I definitely wasn't aware of some of your requirements for the event loop. To respond to some of your concerns:
Again, not sure if this covers your use cases or not.
|
My main concern with that approach is the overhead induced by the use of another thread. About the synchronization issue:
I'm not sure to understand,
I know |
Yes, this would be possible. I'll implement that.
In the current implementation, the event queue doesn't make any guarantees related to timing. Also, the events will always execute in order. So, if an event executes |
Just doing a recurrent task (blinking, logging, whatever...) every x ms while doing other processing is, in my opinion, a valid scenario. It is possible to use While I understand it is not possible to have everything solved and sorted out with this PR, I think it would be nice to have some documentation or examples explaining how to currently:
I can help of these items if you want and if you feel that it is reasonable. |
} | ||
|
||
bool EventLoop::post(const queue_event_t& evt) { | ||
if (_stop_request) { | ||
// An event loop that's stopping doesn't accept more events. | ||
return false; |
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.
One concern, this does limit resuming event queues that are acting as state machines after stopping.
EventQueue queue;
queue.start(); // run for a while
wait_for_important_thing();
queue.stop(); // pause event loop for important thing that may need jitter low
do_important_thing();
queue.start(); // resume running, unfortunately events may have been dropped during important thing
An alternative solution would be to poison the EventLoop with a dummy event (pardon my bad pseudocode):
void EventLoop::dispatch(void) {
while (true) {
e = _queue.get();
if (e.value.p == EVENT_POISON) {
break;
}
}
}
void EventLoop::stop(void) {
post(EVENT_POISON);
thread.join();
}
void EventLoop::start() { | ||
_thread.start(callback(this, &EventLoop::dispatch)); | ||
bool EventLoop::start() { | ||
return _thread.start(callback(this, &EventLoop::dispatch)) == osOK; |
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.
Hmm, should we actually propagate the osStatus
? Or do you think this binds too closesly to the rtos?
Replaced by #2828 |
I think there might still be some useful discussion left in this PR. @pan-, you mentioned having an example showing the use of Ticker and Timeout from the event queue. This is a great idea. It would give me much more confidence in the implementation if one or more of the existing drivers could be updated to use this as a proof of concept. The way I would envision Ticker and Timeout being used in a driver is as resources that are part of a class. This would both make their use apparent and allocate space for them. Any information that needs to be moved out of an ISR could be done by pushing data onto a circular buffer.
|
Description
Adds the Event type and an event loop (EventLoop) for event-based programming.
Status
Main code ready. More tests and more documentation to follow.
Migrations
API changes are backward compatible.