The ROS 2 C++ Executors

TL;DR;
before making the EventsExecutor the default one we need to fix a bug and provide an alternative, backward compatible, mode.


The long version:

There’s currently an important bug that makes the events-executor not usable in combination with simulated time: TimersManager doesn't follow ROS time · Issue #2480 · ros2/rclcpp · GitHub
The client library working group is currently discussing solutions.

Besides the aforementioned bug, there are some concerns that the current implementation of the events-executor has a different ordering for entities execution with respect to the other executors.
This concept applies to situations when an executor thread awakes and there’s more than 1 entity ready for work.

The EventsExecutor always executes entities in the order in which they become ready, while wait-set based executors have a fixed order: they always execute timers first, then subscriptions, etc; moreover there’s an implicit ordering also within each group of entities, based on the order in which entities were added to the executor or node.

Note that this contributes to the difference in performance: wait-set based executors iterate over list of entities and check which one is ready. If you have 50 entities in an executor, but only 1 is receiving messages, you will waste time looping every time (that’s why disabling builtin services/topics improves CPU with wait-set executors).
With the events-executor on the other hand you have a list of ready events each with an associated entity, so every time the executor has work to do, rather than iterating over multiple lists you will have look-ups in a map.

This difference affects also the situation where an entity has more than one item of work to do, such as a subscription that received 2 messages.
The wait-set based executors can’t process 2 items of work from the same entity within the same iteration.
So the SingleThreadedExecutor in this situation will 1) loop through everything and execute the first message, 2) try to go back to sleep 3) wake up immediately and 4) loop through everything again and execute the second message.
On the other hand, whenever the EventsExecutor wakes up, it will execute all the available events before going back to sleep.

IMO the EventsExecutor approach besides being way more performant, is also more correct, as it ensures that all events will be processed as soon as possible.
However, before making the EventsExecutor default, it was requested that we provided an alternative implementation that ensures the same ordering behavior as other executors.

The EventsExecutor design allows to support different behaviors through the definition of custom EventQueue class.
The current implementation in rclcpp just uses a std::queue under the hood, so the idea is to write a new class that internally will sort events as is done by wait-set executors.
This will obviously come at a performance cost (I expect performance to still be a lot better than the current default executor), but users can always switch to different queue implementations.
P.S. There are already other queue implementations in the community:

6 Likes