【ROS2】ROS 2 中 WaitSet(等待集)的简介与使用

发布时间:2026/5/19 23:38:32

【ROS2】ROS 2 中 WaitSet(等待集)的简介与使用 【ROS2】ROS 2 中 WaitSet等待集的简介与使用1、官方示例代码2、代码解析2.1、整体功能总结2.2、核心前置概念2.3、逐模块代码解析2.4、运行逻辑梳理完整流程2.5、应用场景与价值2.6、关键对比WaitSet vs Executor2.7、小结3、技术背景与应用场景3.1、推出时间与版本演进3.2、核心原理3.3、核心适用场景按优先级3.4、不适用场景3.5、与 Executor 的核心对比补充3.6、小结1、官方示例代码#includeiostream#includememory#includerclcpp/rclcpp.hpp#includestd_msgs/msg/string.hpp/* This example creates a subclass of Node and uses a wait-set based loop to wait on * a subscription to have messages available and then handles them manually without an executor */classWaitSetSubscriber:publicrclcpp::Node{public:explicitWaitSetSubscriber(rclcpp::NodeOptions options):Node(wait_set_subscriber,options){rclcpp::CallbackGroup::SharedPtr cb_group_waitsetthis-create_callback_group(rclcpp::CallbackGroupType::MutuallyExclusive,false);autosubscription_optionsrclcpp::SubscriptionOptions();subscription_options.callback_groupcb_group_waitset;autosubscription_callback[this](std_msgs::msg::String::UniquePtr msg){RCLCPP_INFO(this-get_logger(),I heard: %s,msg-data.c_str());};subscription_this-create_subscriptionstd_msgs::msg::String(topic,10,subscription_callback,subscription_options);wait_set_.add_subscription(subscription_);thread_std::thread([this]()-void{spin_wait_set();});}~WaitSetSubscriber(){if(thread_.joinable()){thread_.join();}}voidspin_wait_set(){while(rclcpp::ok()){// Wait for the subscriber event to trigger. Set a 1 ms margin to trigger a timeout.constautowait_resultwait_set_.wait(std::chrono::milliseconds(501));switch(wait_result.kind()){caserclcpp::WaitResultKind::Ready:{std_msgs::msg::String msg;rclcpp::MessageInfo msg_info;if(subscription_-take(msg,msg_info)){std::shared_ptrvoidtype_erased_msgstd::make_sharedstd_msgs::msg::String(msg);subscription_-handle_message(type_erased_msg,msg_info);}break;}caserclcpp::WaitResultKind::Timeout:if(rclcpp::ok()){RCLCPP_WARN(this-get_logger(),Timeout. No message received after given wait-time);}break;default:RCLCPP_ERROR(this-get_logger(),Error. Wait-set failed.);}}}private:rclcpp::Subscriptionstd_msgs::msg::String::SharedPtr subscription_;rclcpp::WaitSet wait_set_;std::thread thread_;};#includerclcpp_components/register_node_macro.hppRCLCPP_COMPONENTS_REGISTER_NODE(WaitSetSubscriber)2、代码解析以上代码是 ROS 2 中基于 WaitSet等待集的手动消息处理示例核心特点是不依赖 ROS 2 默认的 Executor执行器而是通过 WaitSet 主动等待订阅者的消息事件手动接收并处理消息。这种方式能让开发者完全掌控消息处理的时机和线程模型是 ROS 2 进阶编程中 “手动控制事件循环” 的典型实现。2.1、整体功能总结这个程序实现了一个 ROS 2 订阅者节点wait_set_subscriber核心逻辑创建独立的回调组和订阅者将订阅者加入 WaitSet等待集启动一个独立线程在该线程中运行基于 WaitSet 的事件循环循环等待订阅者有消息可用超时时间 501ms有消息时手动 take取出消息调用订阅者的回调函数处理超时无消息时打印警告日志等待失败时打印错误日志节点销毁时等待线程退出保证资源安全释放核心特性脱离 ROS 2 内置的 spin()/spin_some() 执行器完全手动控制消息的等待、接收和处理流程。2.2、核心前置概念在解析代码前先理解 3 个关键概念ROS 2 事件驱动核心WaitSet等待集本质ROS 2 封装的事件等待机制底层对接 DDS 的 WaitSet可监听订阅者、定时器、服务、客户端等事件作用主动阻塞等待 “目标事件触发”如订阅者有消息替代 Executor 的自动事件分发核心方法add_subscription()添加监听的订阅者、wait()阻塞等待事件可设超时。Executor执行器ROS 2 默认的事件处理机制spin()/spin_some() 底层就是 Executor自动监听事件、分发回调本示例完全绕过 Executor用 WaitSet 手动线程实现事件循环灵活性更高但需手动处理所有逻辑。CallbackGroup回调组用于管理回调函数的线程模型示例中创建 MutuallyExclusive互斥回调组保证同一时间只有一个回调执行这里的回调组主要是为订阅者绑定上下文实际消息处理由手动线程完成。2.3、逐模块代码解析头文件引入基础依赖#includeiostream// 基础输入输出示例中未直接使用预留#includememory// 智能指针SharedPtr/UniquePtr#includethread// 线程创建与管理std::thread#includerclcpp/rclcpp.hpp// ROS 2 核心 API#includestd_msgs/msg/string.hpp// ROS 2 标准字符串消息#includerclcpp_components/register_node_macro.hpp// 组件化注册宏节点可编译为组件重点 用于创建独立的事件循环线程rclcpp_components/register_node_macro.hpp 支持将节点注册为 ROS 2 组件可选特性。节点类定义核心逻辑classWaitSetSubscriber:publicrclcpp::Node{public:// 构造函数初始化节点、回调组、订阅者、WaitSet、事件线程explicitWaitSetSubscriber(rclcpp::NodeOptions options):Node(wait_set_subscriber,options)// 节点名 自定义节点选项{// 第一步创建独立的回调组 // 类型MutuallyExclusive互斥第二个参数 false 表示不自动添加到节点的默认执行器rclcpp::CallbackGroup::SharedPtr cb_group_waitsetthis-create_callback_group(rclcpp::CallbackGroupType::MutuallyExclusive,false);// 第二步配置订阅者选项绑定回调组 autosubscription_optionsrclcpp::SubscriptionOptions();subscription_options.callback_groupcb_group_waitset;// 订阅者绑定到自定义回调组// 第三步定义订阅者回调函数Lambda autosubscription_callback[this](std_msgs::msg::String::UniquePtr msg){// 回调逻辑打印收到的消息RCLCPP_INFO(this-get_logger(),I heard: %s,msg-data.c_str());};// 第四步创建订阅者 subscription_this-create_subscriptionstd_msgs::msg::String(topic,// 订阅的话题名10,// 队列大小subscription_callback,// 消息回调函数subscription_options// 自定义选项绑定回调组);// 第五步将订阅者加入 WaitSet监听其消息事件 wait_set_.add_subscription(subscription_);// 第六步启动独立线程运行 WaitSet 事件循环 // 线程执行 spin_wait_set() 方法脱离主线程的 spin()thread_std::thread([this]()-void{spin_wait_set();});}// 析构函数安全释放线程资源 ~WaitSetSubscriber(){// 检查线程是否可连接避免重复 join确保线程退出后再销毁节点if(thread_.joinable()){thread_.join();}}// 核心方法WaitSet 事件循环 voidspin_wait_set(){// 循环条件ROS 2 上下文正常未调用 shutdownwhile(rclcpp::ok()){// 1. 阻塞等待订阅者事件超时时间 501ms// 若订阅者有消息返回 Ready超时返回 Timeout失败返回 Errorconstautowait_resultwait_set_.wait(std::chrono::milliseconds(501));// 2. 根据等待结果处理switch(wait_result.kind()){// 情况 1有消息可用Readycaserclcpp::WaitResultKind::Ready:{// 定义存储消息的变量和消息信息时间戳、发布者等std_msgs::msg::String msg;rclcpp::MessageInfo msg_info;// 手动 take取出消息从订阅者队列中取出一条消息// take 返回 booltrue 表示成功取出false 表示队列空理论上不会发生因 WaitSet 已通知 Readyif(subscription_-take(msg,msg_info)){// 将消息封装为类型擦除的共享指针适配 handle_message 的参数要求std::shared_ptrvoidtype_erased_msgstd::make_sharedstd_msgs::msg::String(msg);// 手动调用订阅者的回调函数处理消息subscription_-handle_message(type_erased_msg,msg_info);}break;}// 情况 2超时Timeoutcaserclcpp::WaitResultKind::Timeout:// 若 ROS 2 上下文仍正常打印超时警告if(rclcpp::ok()){RCLCPP_WARN(this-get_logger(),Timeout. No message received after given wait-time);}break;// 情况 3等待失败Errordefault:RCLCPP_ERROR(this-get_logger(),Error. Wait-set failed.);}}}private:// 成员变量 rclcpp::Subscriptionstd_msgs::msg::String::SharedPtr subscription_;// 订阅者智能指针rclcpp::WaitSet wait_set_;// 等待集监听订阅者事件std::thread thread_;// 事件循环线程};// 组件化注册将节点注册为 ROS 2 组件可选 RCLCPP_COMPONENTS_REGISTER_NODE(WaitSetSubscriber)核心逻辑补充说明take() vs callback普通订阅者由 Executor 自动调用 take() handle_message()本示例中WaitSet 通知 “有消息” 后手动调用 take() 取出消息再调用 handle_message() 触发回调完全复刻 Executor 的核心逻辑但由开发者掌控时机。线程安全事件循环运行在独立线程 thread_ 中与主线程隔离析构函数中 join() 线程避免节点销毁时线程仍在运行导致的资源泄漏。超时设置 501ms预留 1ms 余量避免与 500ms 等常见周期冲突确保超时逻辑能稳定触发。2.4、运行逻辑梳理完整流程程序启动 → 初始化 ROS 2 上下文创建 WaitSetSubscriber 节点节点构造函数执行创建回调组 → 配置订阅者 → 绑定到回调组 → 将订阅者加入 WaitSet启动独立线程执行 spin_wait_set() 方法线程进入 spin_wait_set() 循环调用 wait_set_.wait(501ms) 阻塞等待订阅者消息若发布者向 topic 发布消息WaitSet 检测到事件返回 Ready手动 take() 取出消息调用 handle_message() 触发回调打印消息若 501ms 内无消息WaitSet 返回 Timeout打印超时警告若等待失败如订阅者销毁返回 Error打印错误日志用户按下 CtrlC → rclcpp::ok() 变为 false循环退出节点析构 → join() 线程释放资源程序退出。2.5、应用场景与价值这个示例的核心价值是 脱离 Executor 手动控制事件循环适用于以下场景自定义事件循环需要将 ROS 2 事件消息、定时器集成到外部事件循环如 Qt 事件循环、工业控制循环需精确控制消息处理的时机如每 100ms 批量处理一次消息而非收到即处理。高性能 / 低延迟场景绕过 Executor 的封装直接调用底层 take()/handle_message()减少中间层开销对消息处理线程做精细化调度如绑定到特定 CPU 核心。多事件协同等待WaitSet 可同时监听多个订阅者、定时器、服务端事件实现 “任意一个事件触发就处理” 的逻辑如同时等待传感器消息和控制指令。嵌入式 / 资源受限系统替代笨重的 Executor仅保留必要的 WaitSet 逻辑减少内存 / CPU 占用。调试 / 故障排查手动控制消息接收流程便于断点调试如在 take() 后暂停检查消息内容。2.6、关键对比WaitSet vs Executor维度WaitSet手动Executor自动如 spin()控制粒度极细完全掌控等待 / 接收 / 处理较粗自动分发开发者仅写回调灵活性极高可集成到任意事件循环较低依赖 ROS 2 内置逻辑代码复杂度高需手动处理等待 / 错误 / 线程低一行 spin() 搞定性能略高减少 Executor 封装开销略低额外封装层适用场景进阶 / 定制化需求普通 / 快速开发需求2.7、小结核心功能实现不依赖 Executor 的 ROS 2 订阅者通过 WaitSet 独立线程手动控制消息的等待、接收和处理关键技术rclcpp::WaitSet 事件监听、take() 手动取消息、handle_message() 手动触发回调、独立线程管理核心价值完全掌控消息处理流程适用于自定义事件循环、高性能 / 低延迟、嵌入式系统等进阶场景学习重点理解 ROS 2 事件驱动的底层逻辑Executor 本质是 WaitSet 自动循环是 ROS 2 进阶开发的核心知识点。这个示例是 ROS 2 从 “使用封装” 到 “理解底层” 的关键过渡掌握它能深入理解 ROS 2 事件处理的本质。3、技术背景与应用场景ROS 2 WaitSet等待集推出时间与适用场景3.1、推出时间与版本演进WaitSet 是 ROS 2 从底层对接 DDS 标准的核心特性其演进历程如下底层基础能力随 ROS 2 首个正式版本 Ardent Apalone2017 年 12 月 引入作为 rcl 层ROS Client Library的基础接口直接封装 DDS 的 WaitSet 机制rclcpp 层标准化在 Foxy Fitzroy2020 年 6 月 版本完成 rclcpp::WaitSet 接口定型提供易用的 C 封装如 add_subscription()、wait() 等方法功能完善与稳定Humble Hawksbill2022 年 5 月LTS补充超时处理、多事件监听、错误码标准化成为生产级可用特性Iron Irwini2023 年优化性能支持更多事件类型如定时器、服务、客户端、GuardCondition兼容性所有 ROS 2 版本Ardent 及以后均支持 WaitSet无版本兼容限制是 ROS 2 事件驱动的底层核心。3.2、核心原理WaitSet 是 ROS 2 最底层的事件等待机制本质是开发者将需要监听的事件源订阅者、定时器、服务端、客户端等加入 WaitSet调用 wait() 方法阻塞当前线程直到任意一个事件源触发如订阅者有消息、定时器到期或超时开发者手动处理触发的事件如取出消息、执行定时器逻辑完全掌控事件处理的时机和流程。它是 ROS 2 内置 Executorspin()/spin_some()的底层实现基础——Executor 本质就是 “WaitSet 自动事件分发 线程管理” 的封装。3.3、核心适用场景按优先级自定义事件循环最核心场景集成外部事件循环将 ROS 2 事件消息、定时器融入非 ROS 原生的事件循环如 Qt/QML 事件循环、工业控制周期循环、Unity/Unreal 游戏引擎循环批量事件处理不 “收到消息即处理”而是按固定周期如 100ms批量取出所有待处理消息 / 事件适配高实时性的控制周期单线程多事件协同在一个线程中同时监听 “传感器消息 控制指令 定时器”任意一个事件触发即处理避免多线程同步开销。高性能 / 低延迟场景嵌入式 / 边缘设备绕过 Executor 的封装开销如自动回调分发、线程池管理直接调用底层 take() 取消息减少 CPU / 内存占用硬实时系统手动控制 WaitSet 的等待超时、事件处理时机可绑定到特定 CPU 核心满足微秒级延迟要求如工业机器人运动控制高频消息处理对激光雷达、图像等高频消息通过 WaitSet 精准控制 “取消息→处理→反馈” 的全流程避免 Executor 调度的不确定性。精细化事件控制选择性处理事件监听多个订阅者但仅处理 “优先级高” 的事件如先处理急停指令再处理普通传感器消息自定义超时逻辑为不同事件设置差异化超时如传感器消息超时 100ms 报警控制指令超时 10ms 触发应急逻辑事件依赖处理等待 “多个事件都触发” 后再处理如同时收到 “传感器数据 校准指令” 才执行校准通过多次 WaitSet 等待实现。多事件源统一监听一个 WaitSet 可同时监听多种类型的事件源订阅者消息到达、定时器周期触发、服务端客户端请求、客户端服务响应、GuardCondition手动触发的条件典型场景机器人 “待机状态” 下同时监听 “唤醒指令订阅者、定时唤醒定时器、手动触发GuardCondition”任意一个事件触发即退出待机。调试 / 底层开发深入理解 ROS 2 事件机制通过 WaitSet 手动实现 Executor 的核心逻辑理解 spin() 的底层原理精准调试事件流程在 wait() 后断点检查哪些事件触发、触发时机定位 “消息丢失”“定时器延迟” 等底层问题自定义 Executor基于 WaitSet 开发适配特定场景的 Executor如单线程高优先级 Executor、批量处理 Executor。资源极度受限的场景单片机、裸机移植的 ROS 2 MicroRMW 为 MicroXRCE-DDS仅保留 WaitSet 核心逻辑无需加载完整 Executor大幅降低资源占用。3.4、不适用场景普通快速开发仅需简单订阅 / 发布消息使用 spin()/spin_some() 更高效无需手动编写 WaitSet 循环多线程并发处理需要多个回调并行执行时Executor 的线程池机制比手动管理 WaitSet 多线程更简单对代码复杂度敏感的场景WaitSet 需手动处理事件检测、消息取出、错误处理代码量远大于 Executor。3.5、与 Executor 的核心对比补充维度WaitSet手动Executor自动如 spin()控制粒度极细底层事件级较粗回调级灵活性完全自定义适配任意场景固定逻辑仅可配置线程数代码复杂度高需手动处理等待 / 取消息 / 错误低仅需编写回调函数性能无封装开销更高有调度 / 分发开销略低学习成本高需理解 ROS 2 底层事件模型低开箱即用3.6、小结推出核心节点2017 年随 ROS 2 首个版本引入底层能力2020 年 Foxy 版本完成 C 接口标准化2022 年 Humble 成为稳定的生产级特性核心定位ROS 2 事件驱动的底层基石Executor 是其 “封装后的易用版”最佳场景自定义事件循环、高性能 / 低延迟系统、嵌入式 / 硬实时场景、多事件源统一监听是 ROS 2 进阶开发从 “使用” 到 “定制”的核心工具。

相关新闻