核心概念
Callback Group(回调组)是一个管理一个或多个回调函数执行规则的容器。它决定了这些回调函数是如何被节点(Node)的 executor 调度的,特别是当多个回调函数同时就绪时,它们之间是并行执行还是必须串行执行。
理解回调组的关键在于理解它与 Executor 的交互。Executor 是 ROS 2 中负责检查订阅、定时器、服务、动作服务器等是否就绪,并调用其对应回调函数的组件。
为什么需要 Callback Group?
例如一个节点,它有以下组件:
- 一个激光雷达(Lidar)订阅者:回调函数
lidar_callback
,处理高频、数据量大的点云数据。 - 一个键盘输入订阅者:回调函数
keyboard_callback
,处理用户偶尔的按键输入。 - 一个服务服务器:回调函数
save_data_service_callback
,处理保存数据的请求。
如果没有回调组,所有回调函数默认都在同一个组里。当 Executor 发现多个回调函数同时就绪时(例如,正在处理激光数据时用户按下了键),它会将它们全部放入一个队列,然后逐个执行。
这可能会导致一个问题:处理庞大的激光雷达数据可能会阻塞 keyboard_callback
或 save_data_service_callback
很长时间,导致用户输入或服务请求响应非常迟钝,体验很差。
回调组就是为了解决这种回调函数之间的相互干扰问题而设计的。
回调组的类型
ROS 2 主要提供了两种类型的回调组:
1. Mutually Exclusive (互斥型) - 默认类型
- 行为:属于同一个互斥组的所有回调函数不能同时执行。Executor 会像处理一个队列一样,一次只执行其中一个。
- 类比:单线程。任务一个接一个地做。
- 适用场景:这是最常用也是默认的类型。适用于回调函数之间没有严格的实时性要求,或者它们会访问共享资源需要互斥锁保护的情况。
2. Reentrant (可重入型)
- 行为:属于同一个可重入组的所有回调函数可以同时被执行。Executor 会尽可能同时调用它们(如果系统有多个CPU核心,则真正并行)。
- 警告:使用此类型需要非常小心。你必须确保回调函数是线程安全的。如果它们会访问共享的变量或资源,你必须自己使用锁(如
std::mutex
)来保护,否则会导致数据竞争和未定义行为。 - 类比:多线程。多个任务可以同时进行。
- 适用场景:适用于那些相互独立、没有共享数据、且对实时性要求很高的回调函数。例如,一个控制电机的高频定时器回调和一个发布状态的低频定时器回调。
使用回调组
你需要在创建订阅者、定时器、服务等之前先创建回调组,然后在创建这些对象时将回调组作为参数传入。
以下是 C++ 和 Python 的示例代码:
C++ 示例
#include “rclcpp/rclcpp.hpp”class MyNode : public rclcpp::Node {
public:MyNode() : Node(”my_node”) {// 1. 创建回调组// 互斥型 (默认就是这个,显式写出更清晰)mutually_exclusive_group_ = this- >create_callback_group(rclcpp::CallbackGroupType::MutuallyExclusive);// 可重入型reentrant_group_ = this- >create_callback_group(rclcpp::CallbackGroupType::Reentrant);// 2. 设置选项,在创建对象时指定所属的回调组rclcpp::SubscriptionOptions options;options.callback_group = mutually_exclusive_group_;// 创建订阅者,并指定它属于 mutually_exclusive_group_lidar_sub_ = this- >create_subscription< sensor_msgs::msg::LaserScan >(”/scan”, 10,std::bind(&MyNode::lidar_callback, this, std::placeholders::_1),options); // 传入 options// 对于不需要特殊选项的对象,可以直接传递 callback_group 参数// 这个定时器属于 reentrant_group_,可以和上面的订阅者并行执行timer_ = this- >create_wall_timer(std::chrono::seconds(1),std::bind(&MyNode::timer_callback, this),reentrant_group_); // 直接传入回调组}private:void lidar_callback(const sensor_msgs::msg::LaserScan::SharedPtr msg) {// 处理激光数据,可能很耗时}void timer_callback() {// 定时执行的任务}rclcpp::CallbackGroup::SharedPtr mutually_exclusive_group_;rclcpp::CallbackGroup::SharedPtr reentrant_group_;rclcpp::Subscription< sensor_msgs::msg::LaserScan >::SharedPtr lidar_sub_;rclcpp::TimerBase::SharedPtr timer_;
};int main(int argc, char * argv[]) {rclcpp::init(argc, argv);auto node = std::make_shared< MyNode >();rclcpp::spin(node);rclcpp::shutdown();return 0;
}
回调组与执行器(Executor)的配合
- SingleThreadedExecutor:即使是可重入组,回调函数也无法真正并行,因为只有一个线程。但 Executor 会通过技巧(如在等待服务响应时切换到其他可执行回调)来模拟并发,提高响应效率。
- MultiThreadedExecutor:这是发挥可重入组威力的地方。该执行器内部有一个线程池。当一个可重入组有多个回调就绪时,执行器可以从线程池中取出多个线程来真正并行地执行它们。
最佳实践:如果你使用了可重入回调组,通常应该配合 MultiThreadedExecutor
来使用,以实现真正的并行处理。
总结
特性 | Mutually Exclusive (互斥) | Reentrant (可重入) |
---|---|---|
执行方式 | 串行 | 并行 |
线程安全 | 不需要额外考虑(默认安全) | 必须自行保证线程安全 |
性能 | 可能因长回调导致阻塞 | 响应性更好,资源利用率更高 |
适用场景 | 默认选择,共享资源需保护 | 实时性要求高,回调间完全独立 |
核心:Callback Group 是一种强大的工具,让你能够精细地控制节点内部回调函数的执行流程,从而优化节点的响应性和性能,避免不必要的阻塞。在设计复杂的节点时,合理地使用回调组是非常重要的一环。