Modeling your applications with actors, you can get two benefits: messaging and supervising between actors.
Actor is a reactive entity with a state, which can receive and send messages; actor state is usually changed with new messages. Every message has destination (one or more actors) and user-defined payload. Messaging is send with fire-and-forget approach, i.e. sender does not care about further delivery and processing of the message, like with UDP protocol.
Like in UDP a message can be "lost", because the destination queue if full. Unlike UDP, this can be checked, howerver.
The message delivery is asynchronous, as the message is put into the queue, and will be delivered some time later.
With all that properties it is possible to assemble concurrent programs.
Supervising is more about proper actor initialization order, synchronization and failure handling. Synchronization makes it sure that starting sending messages to actors is performed when actors are ready. Failure handling allows to use different restart strategies of a failing actor, or group of actors, or even escalate locally unresolvable problem into upper layers... upto the whole board restart.
rotor-light is platform-neutral framework, i.e. it can be build and run on any plaform, which conforms the requirements (C++17, basically).
There is a successful C++ actor microframework rotor, when a few colleagues of mine asked me to have something similar for embedded systems.
The following table highlights the differences
rotor light | rotor | |
---|---|---|
max. number of actors | 64 | unlimited, runtime |
max. number of messages | compile-time defined | unlimited, runtime |
thread-safety | no | yes |
message priorities | yes | no |
request-response pattern | no | yes |
actors discovery & linking | no | yes |
multiple I/O backends | no (1) | yes |
timers | yes (1) | yes |
non-intrusiveness (2) | yes | yes |
dynamic allocations | no | yes |
C++ standard | C++17 | C++17 |
dependencies | no (except, stdlib++) | boost, event-loops |
multiple addresses per actor | no | yes |
multiple recipients | yes, via broadcasting | yes |
uses RTTI | no | yes |
(1) rotor-light needs only "now" function from timer, the time point and time interval are abstract notions, have to be defined by end-user of the library.
(2) Non-intrusiveness means, that a framework does not owns execution thread: when all messages have been processed, it just exits as there is nothing to do. That way it is possible to integrate a framework with other event-loops or enter into power-save mode for MCU or even let it go into sleep and wakeup on external event (interrupt).
There are a lot of useful patterns and explanations in rotor documentation, it is worth familiarize self with it to get some insights.
rotor-light uses
cmake build system. It has no dependencies,
except C++17 core libraries (e.g. <tuple>
,<type_traits>
etc.). Basically, the
framework can be build via
mkdir build
cd build
cmake ..
make -j4
There are few customization options:
ROTOR_LIGHT_FW_QUEUE
- the queue/priority to be used for internal rotor-light
messaging, 0
by default. This might be useful, to have a dedicated queue for
rotor-light messages, to avoid
messages loss due to queue overfill, or have lower or higher priorities for
rotor-light messages.
ROTOR_LIGHT_ACTOR
- defines the type to be used as actor id, the default is
uint64_t
, which means the limitation of 64 actors in total. If you know a priory,
that there are less than 32/16/8 actors in total, you can save bits here.
ROTOR_LIGHT_TIMEPOINT
defines the type to be used as timepoint. The default
value is int64_t
.
ROTOR_LIGHT_DURATION
defines the type to be used as time interval. The default
value is std::int_fast32_t
.
ROTOR_LIGHT_MESSAGE
- defines the type to be used as message type id, the default is
std::uint_fast16_t
; you can tune that if you have more than 2^16 different message
types.
ROTOR_LIGHT_EVENT
defines the type to be used as event id. The default
value is std::uint16_t
; tune if there is a chance of having more than 2^16
active events at one time.
ROTOR_LIGHT_INDEX
defines the type to be used as queue index. The default
value is std::uint_fast8_t
.
ROTOR_LIGHT_DOC
- generate doxygen docs, false by default.
Usage example:
cmake -DROTOR_LIGHT_TIMEPOINT=int32_t ..
If any internal rotor-light
message is not delivered, this is a critical error. In the case the following function
is invoked in the rotor_ligth
namespace:
namespace rotor_light {
__attribute__((weak)) void on_queue_full() {
for (;;) {
__asm__("nop");
}
}
If there is need to turn on red LED or something like that, then you should define
your own function void on_queue_full()
(without weak specifier) in the namespace.
Custom message delivery faulure does not causes on_queue_full()
invokation, it
should be done manually if needed.
A few advices to avoid the critical error:
inrease queue size
use different queue/prirority for rotor_light
messages, if it is OK to loose
your custom messeages, as it is fatal to loose rotor_ligth
messages. See
ROTOR_LIGHT_FW_QUEUE
build option.