Hi Quantum Leaps, very excited to see the interest in asynchronous paradigms on embedded systems.
The mbed-events library is more a part of a collection of building block than a model for developing asynchronous applications. mbed-events is a piece of a puzzle that we haven't quite figured out yet and I'm excited to see what everyone will build with all the pieces we have in mbed OS. As new programming strategies emerge I'm sure we will work to provide the tools necessary for their realization, but we do have to move cautiously, since technicaly debt is a real concern that limits our flexibility.
The article you posted is an interesting read, and we are trying to encourage a similar object-oriented approach to composing event loops. Here's what Herb Sutter's "Active Object" class might look like implemented with mbed-events:
Active Object using mbed-events
class Active {
private:
EventQueue _queue;
Thread _thread;
public:
// Constructor spawns object-specific thread in background
Active() {
_thread.start(callback(&_queue, &EventQueue::dispatch_forever));
}
// Destructor waits for pending events to finish and gracefully cleans up thread
~Active() {
_queue.break_dispatch();
_thread.join();
}
// Send passes messages to thread running in the background
void Send(Callback<void()> m) {
_queue.call(m);
}
}
However, there are some concerns with the base implementation of Active Objects, notably the resources used by each object to maintain its own thread. With the tools available in mbed-events, you could also create composable Active Objects that can share the execution resources of a parent event queue:
Active Object using mbed-events with shared resources
class Active {
private:
EventQueue _queue;
Thread *_thread;
public:
// Constructor chains event queue to parent if provided, otherwise is lazily allocates a thread
Active(Active *parent = NULL)
: _thread(NULL) {
if (parent) {
_queue.chain(&parent->_queue);
} else {
_thread = new Thread;
_thread->start(callback(&_queue, &EventQueue::dispatch_forever));
}
}
// Destructor will unchain the event queue from its parent if necessary
~Active() {
if (_thread) {
_queue.break_dispatch();
_thread->join();
delete _thread;
}
}
// Send passes messages to thread running in the background
void Send(Callback<void()> m) {
_queue.call(m);
}
}
I'm not sure I understand the downside of associating events with callbacks. Callbacks were chosen as the purest form of computation in an effort to provide the most flexibility. You should be able to share functions between states by passing different context where needed. Let us know if I'm just misunterstanding the problem because I'd be interested to know what I'm missing. Here's a simple FSM written with mbed-events:
Simple FSM using mbed-events
class StopLight {
private:
EventQueue _queue;
int _pending;
enum state { RED, GREEN, YELLOW };
// The _step function is a traditional FSM which enqueues transitions onto the event queue
void _step(state transition) {
switch (transition) {
case RED:
set_red();
_pending = _queue.call_in(1000, this, &StopLight::_step, GREEN);
break;
case GREEN:
set_green();
_pending = _queue.call_in(1000, this, &StopLight::_step, YELLOW);
break;
case YELLOW:
set_yellow();
_pending = _queue.call_in(500, this, &StopLight::_step, RED);
break;
}
}
public:
// The firetruck function can immediately change states in the FSM
void firetruck() {
_queue.cancel(_pending);
_queue.call(this, &StopLight::_step, GREEN);
}
}
While it is good that mbed OS starts to provide support for event-driven programming, it seems to be painstakingly reinventing the wheel, which has been long known as the "Active Object" design pattern. A good article that describes the pattern is "Prefer Using Active Objects Instead of Naked Threads" by Herb Sutter (see http://www.drdobbs.com/parallel/prefer-using-active-objects-instead-of-n/225700095 )
The mbed "event loop library" is a step in the right direction, because it decouples the event-production from event-processing (the central characteristics of the "Active Object" pattern). But unfortunately, the mbed library seems to repeat most of the mistakes that were made in the early event-driven systems and now recognized as sub-optimal.
For example, the events are "attached" to the specific callbacks. This hinders the use of state machines. You can see it easily in the event/state table representation of state machines, where an event-specific callback corresponds to the slice through the table that contains pieces of all the states. So, you have a piece of each state in every event-handler. A much better, more intuitive and efficient view of a state machine is state-centric, where the state-specific behavior is grouped together. In other words, the library should not slice the state machine, but instead should dispatch all events from a given event-queue to a "state machine processor".
And, of course, the next logical step would be to provide support for implementing state machines, preferably hierarchical state machines (a.k.a. UML statecharts)...