关于 Executor
ROS 2 中的执行管理由 Executor 处理。
Executor 借助底层操作系统的一个或多个线程,在收到消息和事件时调用 subscription(订阅)、timer(定时器)、服务端、Action 服务端等的回调函数。
显式的 Executor 类(rclcpp 中的 executor.hpp、rclpy 中的 executors.py,或 rclc 中的 executor.h)尽管基本 API 与 ROS 1 中的 spin 机制非常相似,但提供了更精细的执行管理能力。
下文将重点介绍 C++ 客户端库 rclcpp。
在最简单的情况下,主线程通过调用 rclcpp::spin(..) 来处理 Node 的传入消息和事件,如下所示:
int main(int argc, char* argv[]){ // 一些初始化。 rclcpp::init(argc, argv); ...
// 实例化一个 node。 rclcpp::Node::SharedPtr node = ...
// 运行 executor。 rclcpp::spin(node);
// 关闭并退出。 ... return 0;}调用 spin(node) 本质上等价于实例化并调用单线程 Executor(Single-Threaded Executor),这是最简单的 Executor:
rclcpp::executors::SingleThreadedExecutor executor;executor.add_node(node);executor.spin();调用 Executor 实例的 spin() 后,当前线程会不断轮询 rcl 和中间件层中的传入消息及其他事件,并调用相应的回调函数,直到节点关闭。
为了避免与中间件的 QoS 设置冲突,传入的消息不会缓存在客户端库层的队列中,而是保留在中间件中,等待回调函数取出处理。
(这是与 ROS 1 的关键区别。)
wait set 用于通知 Executor 中间件层上有可用消息,每个队列对应一个二进制标志。
wait set 还用于检测 timer 何时到期。

单线程 Executor 也用于 组件 的容器进程,即在没有显式 main 函数而创建和执行节点的所有场景中。
Executor 的类型
Section titled “Executor 的类型”目前,rclcpp 提供三种 Executor 类型,均派生自一个共享的父类:
digraph Flatland {
Executor -> SingleThreadedExecutor [dir = back, arrowtail = empty]; Executor -> MultiThreadedExecutor [dir = back, arrowtail = empty]; Executor -> EventsCBGExecutor [dir = back, arrowtail = empty]; Executor [shape=polygon,sides=4]; SingleThreadedExecutor [shape=polygon,sides=4]; MultiThreadedExecutor [shape=polygon,sides=4]; EventsCBGExecutor [shape=polygon,sides=4];
}*多线程 Executor(Multi-Threaded Executor)*创建可配置数量的线程,以允许并行处理多个消息或事件。
所有 Executor 都可以通过对每个节点调用 add_node(..) 来使用多个节点。
rclcpp::Node::SharedPtr node1 = ...rclcpp::Node::SharedPtr node2 = ...rclcpp::Node::SharedPtr node3 = ...
rclcpp::executors::SingleThreadedExecutor executor;executor.add_node(node1);executor.add_node(node2);executor.add_node(node3);executor.spin();在上面的示例中,单线程 Executor 的一个线程同时为三个节点提供服务。 在多线程 Executor 的情况下,实际的并行度取决于回调组(callback group)。
回调组事件 Executor(Callback Group Events Executor)
Section titled “回调组事件 Executor(Callback Group Events Executor)”从历史上看,单线程和多线程 Executor 虽然实现简单、执行逻辑易于推演,但 是一个重要的性能瓶颈。
从 Lyrical Luth 版本开始提供的 EventsCBGExecutor 是多年来为降低 CPU 开销而进行的工作成果。 它不再使用 wait set,而是利用先进先出事件队列(first-in first-out events queue)来处理就绪事件。 其核心理念是”不为你不使用的东西买单”——实体不再通过轮询变化来检测,而是在变为就绪状态时才将事件加入队列等待处理。
该 Executor 支持多个 ROS 时间源(包括仿真时间),解决了 慢速运行 timer 导致 timer 事件突发的问题,并提供多线程支持。
因此,该 Executor 也可以作为 MultiThreadedExecutor 的直接替代品,因为它同样会创建可配置数量的工作线程来处理就绪事件。
在单线程模式下,EventsCBGExecutor 减少了上下文切换,并 展现出优异的性能表现。
需要注意的是:目前队列中可添加的事件数量没有上限。这意味着当进程过载、处理速度变慢时,队列中就绪实体的数量可能会无限增长。
rclcpp::Node::SharedPtr node1 = ...rclcpp::Node::SharedPtr node2 = ...rclcpp::Node::SharedPtr node3 = ...rclcpp::Node::SharedPtr node4 = ...
rclcpp::executors::EventsCBGExecutor st_cbg_exec(rclcpp::ExecutorOptions(), 1);st_cbg_exec.add_node(node1);st_cbg_exec.add_node(node2);st_cbg_exec.spin();
rclcpp::executors::EventsCBGExecutor mt_cbg_exec;mt_cbg_exec.add_node(node3);mt_cbg_exec.add_node(node4);mt_cbg_exec.spin();在上面的示例中,st_cbg_exec 将使用一个线程处理 node1 和 node2 的所有就绪事件,而 mt_cbg_exec 将使用系统上可用的最大线程数来处理 node3 和 node4 的就绪事件。
与多线程 Executor 一样,当 CBG Executor 使用多个线程时,实际的并行度取决于回调组。
回调组(Callback groups)
Section titled “回调组(Callback groups)”ROS 2 允许将一个节点的回调组织为若干组。
在 rclcpp 中,可以通过 Node 类的 create_callback_group 函数创建这样的回调组。
在 rclpy 中,通过调用特定回调组类型的构造函数来完成相同的操作。
回调组必须在节点执行期间一直保持引用(例如作为类成员),否则 Executor 将无法触发回调。
然后,可以在创建 subscription、timer 等时指定此回调组——例如通过 subscription options:
my_callback_group = create_callback_group(rclcpp::CallbackGroupType::MutuallyExclusive);
rclcpp::SubscriptionOptions options;options.callback_group = my_callback_group;
my_subscription = create_subscription<Int32>("/topic", rclcpp::SensorDataQoS(), callback, options);Python
Section titled “Python”my_callback_group = MutuallyExclusiveCallbackGroup()my_subscription = self.create_subscription(Int32, "/topic", self.callback, qos_profile=1, callback_group=my_callback_group)所有在未指定回调组的情况下创建的 subscription、timer 等,都会被分配到默认回调组(default callback group)。
默认回调组可以通过 rclcpp 中的 NodeBaseInterface::get_default_callback_group() 和 rclpy 中的 Node.default_callback_group 查询。
回调组有两种类型,类型必须在实例化时指定:
- 互斥(Mutually exclusive): 该组的回调不能并行执行。
- 可重入(Reentrant): 该组的回调可以并行执行。
不同回调组的回调始终可以并行执行。 多线程 Executor 将其线程作为线程池,根据上述条件尽可能并行地处理回调。 有关如何高效使用回调组的提示,请参阅 使用回调组。
rclcpp 中的 Executor 基类还有 add_callback_group(..) 函数,允许将回调组分配给不同的 Executor。
通过使用操作系统调度器配置底层线程,可以优先处理特定的回调而非其他回调。
例如,控制循环的 subscription 和 timer 可以优先于节点的所有其他 subscription 和常规服务。
examples_rclcpp_cbg_executor package 提供了此机制的演示。
如果回调的处理时间短于消息和事件发生的周期,Executor 基本上会按 FIFO(先进先出)顺序依次处理。 但如果某些回调的处理时间较长,消息和事件会在协议栈的较低层排队。 wait set 机制仅向 Executor 报告这些队列的非常有限的信息。 具体来说,它仅报告某个话题是否有消息。 Executor 使用此信息以轮转(round-robin)方式处理消息(包括服务和动作),而不是按 FIFO 顺序。 以下流程图展示了这种调度语义。

这种调度语义最初在 Casini 等人在 ECRTS 2019 上的论文中描述。 (注意:该论文还解释了 timer 事件优先于所有其他消息。 此优先级在 Eloquent 中被移除。)
尽管 rclcpp 的单线程和多线程 Executor 对大多数应用都能很好地工作,但仍存在一些问题,使其不适合实时应用——这类应用需要明确定义的执行时间、确定性以及对执行顺序的自定义控制。 以下是其中一些问题的总结:
- 复杂且混合的调度语义。理想情况下,你需要明确定义的调度语义来执行正式的时序分析。
- 回调可能遭受优先级反转。高优先级回调可能被低优先级回调阻塞。
- 没有对回调执行顺序的显式控制。
- 缺乏对特定 topic(话题)触发的内置控制。
此外,单线程和多线程 Executor 在 CPU 和内存使用方面的开销也不可忽视。
这些问题已通过以下发展得到部分解决:
- rclcpp WaitSet:rclcpp 的
WaitSet类允许直接在 subscription、timer、服务端、Action 服务端等上等待,而不是使用 Executor。它可用于实现确定性的、用户定义的处理序列,可以将来自不同 subscription 的多个消息一起处理。examples_rclcpp_wait_set package 提供了几个使用此用户级 wait set 机制的示例。 - rclc Executor:这个来自 C 客户端库 rclc 的 Executor(为 micro-ROS 开发)赋予用户对回调执行顺序的细粒度控制,并允许自定义触发条件来激活回调。此外,它还实现了逻辑执行时间(Logical Execution Time,LET)语义的思想。
- 回调组事件 Executor(The Callback Group Events Executor)的 CPU 开销比单线程和多线程 Executor 低 10% 到 15%,并使用 FIFO 队列来处理事件。
- Michael Pöhnl 等:“ROS 2 Executor: How to make it efficient, real-time and deterministic?”。ROS World 2021 工作坊。线上活动。2021 年 10 月 19 日。
- Ralph Lange:“Advanced Execution Management with ROS 2”。ROS Industrial Conference。线上活动。2020 年 12 月 16 日。
- Daniel Casini, Tobias Blass, Ingo Lütkebohle, 和 Björn Brandenburg:“Response-Time Analysis of ROS 2 Processing Chains under Reservation-Based Scheduling”,Proceedings of 31st ECRTS 2019,德国斯图加特,2019 年 7 月。