ROS2 中的执行管理由 执行器(Executor) 处理。执行器利用底层操作系统的一个或多个线程,在接收到消息和事件时调用订阅者、定时器、服务服务器、动作服务器的回调函数。显式的Executor
类(rclcpp 中的executor.hpp
、rclpy 中的executors.py
或 rclc 中的executor.h
)提供了与ROS1 API 非常相似的,但是比 ROS1 中spin
机制更精细的执行管理控制。
以下我们主要关注 C++ 客户端库 rclcpp。
基本用法
最简单的情况是通过调用rclcpp::spin(..)
,使用主线程处理节点的传入消息和事件,示例如下:
cpp
int main(int argc, char* argv[])
{
// 初始化
rclcpp::init(argc, argv);
...
// 创建节点
rclcpp::Node::SharedPtr node = ...
// 运行执行器
rclcpp::spin(node);
// 关闭并退出
...
return 0;
}
调用spin(node)
本质上等同于实例化并调用单线程执行器(Single-Threaded Executor),这是最简单的执行器:
cpp
rclcpp::executors::SingleThreadedExecutor executor;
executor.add_node(node);
executor.spin();
调用执行器实例的spin()
后,当前线程会开始查询 rcl
和中间件层的传入消息和其他事件,并调用相应的回调函数,一直到节点关闭。为了不影响中间件的 QoS 设置,传入消息不会存储在客户端库层的队列中,而是保留在中间件中,直到被回调函数处理(这里是与 ROS1 的关键区别)。执行器通过等待集(wait set) 感知中间件层的可用消息,每个队列对应一个二进制标志位,等待集也用于检测定时器是否到期。

在所有无需显式主函数创建和执行节点的场景中,单线程执行器也用于组件的容器进程。
执行器类型
当前,rclcpp 提供了三种执行器类型,均派生自同一个父类:

其中多线程执行器(Multi-Threaded Executor) 会创建可配置数量的线程,允许并行处理多条消息或事件。 而静态单线程执行器(Static Single-Threaded Executor) 针对节点的订阅、定时器、服务服务器、动作服务器等结构的扫描进行了运行时优化。它仅在添加节点时执行一次扫描,而另外两种执行器则会定期扫描这些变化。因此,静态单线程执行器应该仅仅在节点初始化时创建所有订阅、定时器等资源的场景中使用。
所有三种执行器都可以通过调用 add_node(..)
添加多个节点:
cpp
rclcpp::Node::SharedPtr node1 = ...;
rclcpp::Node::SharedPtr node2 = ...;
rclcpp::Node::SharedPtr node3 = ...;
rclcpp::executors::StaticSingleThreadedExecutor executor;
executor.add_node(node1);
executor.add_node(node2);
executor.add_node(node3);
executor.spin();
在上面这个例子中,静态单线程执行器的单个线程用于共同服务三个节点。对于多线程执行器,实际并行性取决于回调组(Callback Groups)。
回调组
ROS2 允许将节点的回调组织成组。在 rclcpp 中,可通过 Node
类的 create_callback_group
函数创建回调组;在 rclpy 中,则通过调用特定回调组类型的构造函数实现。回调组必须在节点的整个执行周期中保持存活(例如作为类成员),否则执行器无法触发回调。创建订阅、定时器等资源时,可通过选项指定回调组,例如:
C++
cpp
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
python
my_callback_group = MutuallyExclusiveCallbackGroup()
my_subscription = self.create_subscription(Int32, "/topic", self.callback, qos_profile=1,
callback_group=my_callback_group)
所有创建时未指定回调组的订阅、定时器等资源,都会被分配到默认回调组。在 rclcpp 中,可通过 NodeBaseInterface::get_default_callback_group()
查询默认回调组;在 rclpy 中则通过 Node.default_callback_group
获取。
回调组有两种类型,需在实例化时指定:
- 互斥组(Mutually exclusive):该组的回调禁止并行执行。
- 可重入组(Reentrant):该组的回调允许并行执行。
不同回调组的回调始终可以并行执行。多线程执行器会根据这些规则,利用线程池尽可能并行处理回调。有关高效使用回调组的建议,可以参考《使用回调组》。
rclcpp 的执行器基类还提供 add_callback_group(..)
函数,允许将回调组分配给不同执行器。通过操作系统调度器配置底层线程,可对特定回调设置优先级(例如,控制回路的订阅和定时器可优先于节点的其他订阅和标准服务)examples_rclcpp_cbg_executor
(github.com/ros2/exampl...) 软件包提供了该机制的演示。
调度语义
如果回调的处理时间短于消息和事件的产生周期,执行器基本按 先进先出(FIFO) 顺序处理它们。而如果某些回调处理时间较长,消息和事件会在底层栈中排队。等待集(Wait Set)机制仅向执行器报告这些队列的少量信息(例如是否有某个主题的消息)。执行器基于此信息以 轮询(Round-Robin) 而非 FIFO 顺序处理消息、服务和动作。以下流程图展示了该调度语义:

这种语义最早由 Casini 等人在 ECRTS 2019 会议的论文(drops.dagstuhl.de/opus/vollte...) 中描述(注:该论文还提到定时器事件优先级高于其他消息,但这一优先级在 Eloquent 版本中已移除(github.com/ros2/rclcpp...)%25E3%2580%2582 "https://github.com/ros2/rclcpp/pull/841))%E3%80%82")
未来展望
尽管 rclcpp 的三种执行器适用于大多数应用,但仍存在一些不适合实时应用的问题(实时应用需要明确的执行时间、确定性和对执行顺序的自定义控制),主要问题包括:
- 复杂且混合的调度语义,理想情况下需要明确的调度语义进行形式化时序分析。
- 回调可能出现优先级反转,高优先级回调可能被低优先级回调阻塞。
- 无法显式控制回调执行顺序。
- 缺乏对特定主题触发的内置控制。
- 执行器在 CPU 和内存使用方面的开销较大,静态单线程执行器虽然大幅减少了开销,但对某些应用可能仍然不足。
下面这两个功能部分解决了这些问题:
- rclcpp WaitSet :rclcpp 的
WaitSet
类允许不使用执行器,而直接等待订阅、定时器、服务服务器、动作服务器等资源。它可用于实现确定的用户自定义处理序列,甚至同时处理来自不同订阅的多条消息。examples_rclcpp_wait_set
(github.com/ros2/exampl...) 软件包提供了多个使用该用户级等待集机制的示例。 - rclc 执行器:C 客户端库 rclc(为 micro-ROS 开发)的执行器允许用户细粒度控制回调的执行顺序,并支持自定义触发条件激活回调。此外,它实现了逻辑执行时间(LET)语义的思想。
欢迎关注【智践行】,发 【机器人】 获得机器人经典学习资料