ROS2 --- WaitSet(等待集) 等待实体就绪,管理执行回调函数 📅 2026/8/26 16:33:38 一、WaitSet的定位1.1 WaitSet的作用ROS2默认的执行器如SingleThreadedExecutor、MultiThreadedExecutor会自动管理回调的触发执行器内部维护一个WaitSet不断调用wait()等待实体就绪然后自动执行回调函数。但在以下场景中默认执行器的“自动性”会成为限制实时性要求高的场景如机器人运动控制需要手动控制回调执行的优先级和时机自定义等待逻辑比如同时等待“话题消息”和“服务请求”且需按特定顺序处理嵌入式/资源受限场景需要精简执行器开销手动管理等待流程。WaitSet的本质是ROS2封装的DDS层等待集原语用于监听一组“可等待实体Waitable”的状态当任意实体进入“就绪状态”时wait()方法返回开发者可手动处理该实体的逻辑。1.2 定义WaitSet一个容器可添加多个Waitable实体提供wait()方法阻塞等待任意实体就绪返回就绪实体集合Waitable实体ROS2中可被WaitSet监听的对象包括Subscription话题订阅者就绪条件有新消息到达且未被读取ServiceServer服务端就绪条件有新的服务请求到达Client客户端就绪条件服务端返回响应Timer定时器就绪条件定时周期到期GuardCondition守护条件就绪条件手动调用trigger()触发Event事件对象如话题匹配状态变化、QoS违规事件。就绪状态Ready实体满足“可处理”的条件且该状态会在wait()返回后被重置需重新等待下一次就绪。1.3 底层DDS关联ROS2基于DDSData Distribution Service实现通信WaitSet直接封装了DDS的DDS_WaitSet接口DDS的WaitSet是分布式系统中同步等待数据/事件的标准原语ROS2在DDS WaitSet基础上封装了面向ROS2实体如Subscription、Timer的接口屏蔽了DDS底层细节所有Waitable实体最终都会被转换为DDS的Condition对象添加到DDS WaitSet中等待。二、WaitSet的CAPI2.1 头文件使用WaitSet需包含以下头文件根据使用的实体类型补充#includerclcpp/wait_set.hpp// WaitSet核心头文件#includerclcpp/subscription.hpp// 订阅者#includerclcpp/timer.hpp// 定时器#includerclcpp/guard_condition.hpp// 守护条件#includerclcpp/service.hpp// 服务端/客户端2.2 APIWaitSet的C API围绕“添加实体→等待就绪→处理实体→重置”的流程设计方法功能注意事项add_subscription(Subscription::SharedPtr sub)添加话题订阅者重复添加同一实体不会报错但会冗余监听add_timer(TimerBase::SharedPtr timer)添加定时器定时器需先通过create_timer()创建add_guard_condition(GuardCondition::SharedPtr gc)添加守护条件守护条件需手动触发add_service(ServiceBase::SharedPtr srv)添加服务端仅监听服务请求就绪add_client(ClientBase::SharedPtr client)添加客户端仅监听服务响应就绪remove_xxx(...)移除对应实体移除不存在的实体返回falseclear()清空所有实体清空后WaitSet无监听对象wait(std::chrono::durationT timeout)阻塞等待实体就绪超时时间可设为rclcpp::Duration::max()无限等待contains(Waitable::SharedPtr entity)检查实体是否在WaitSet中可结合ready_set检查是否就绪wait()的返回值wait()返回std::pairrclcpp::WaitResult, rclcpp::ReadySetWaitResult枚举类型标识等待结果rclcpp::WaitResult::Ready至少一个实体就绪rclcpp::WaitResult::Timeout超时无实体就绪rclcpp::WaitResult::Error等待出错如实体已销毁ReadySet就绪实体的集合可通过get_subscriptions()、get_timers()等方法获取对应类型的就绪实体。2.3 使用示例以下示例实现“同时等待话题消息、定时器、守护条件”手动处理就绪逻辑覆盖90%的常用场景#includerclcpp/rclcpp.hpp#includestd_msgs/msg/string.hpp#includerclcpp/wait_set.hpp#includerclcpp/guard_condition.hppusingnamespacestd::chrono_literals;intmain(intargc,char*argv[]){// 1. 初始化ROS2节点rclcpp::init(argc,argv);autonoderclcpp::Node::make_shared(waitset_demo_node);// 2. 创建可等待实体// 2.1 话题订阅者无回调手动取消息autosubnode-create_subscriptionstd_msgs::msg::String(test_topic,10,[](conststd_msgs::msg::String::SharedPtr){});// 空回调// 2.2 定时器1秒周期autotimernode-create_wall_timer(1s,[](){});// 空回调手动触发// 2.3 守护条件手动触发autogcstd::make_sharedrclcpp::GuardCondition();// 3. 初始化WaitSet并添加实体rclcpp::WaitSet wait_set;wait_set.add_subscription(sub);wait_set.add_timer(timer);wait_set.add_guard_condition(gc);// 4. 循环等待并处理就绪实体while(rclcpp::ok()){// 4.1 等待超时时间2秒也可设为无限等待rclcpp::Duration::max()auto[wait_result,ready_set]wait_set.wait(2s);// 4.2 处理等待结果if(wait_resultrclcpp::WaitResult::Error){RCLCPP_ERROR(node-get_logger(),WaitSet等待出错);break;}elseif(wait_resultrclcpp::WaitResult::Timeout){RCLCPP_INFO(node-get_logger(),WaitSet超时2秒无实体就绪);continue;}// 4.3 处理就绪实体按优先级定时器→守护条件→话题// 处理定时器if(ready_set.get_timers().count(timer)){RCLCPP_INFO(node-get_logger(),定时器就绪执行定时逻辑);timer-execute_callback();// 手动执行定时器回调timer-reset();// 重置定时器可选根据需求}// 处理守护条件if(ready_set.get_guard_conditions().count(gc)){RCLCPP_INFO(node-get_logger(),守护条件被触发);// 守护条件触发后需手动重置否则下次wait会直接就绪gc-reset();}// 处理话题订阅者if(ready_set.get_subscriptions().count(sub)){std_msgs::msg::String msg;rclcpp::MessageInfo msg_info;// 手动读取话题消息核心替代自动回调if(sub-take(msg,msg_info)){RCLCPP_INFO(node-get_logger(),收到话题消息%s,msg.data.c_str());}}}// 5. 清理资源wait_set.clear();rclcpp::shutdown();return0;}代码解释订阅者使用空回调因为WaitSet模式下手动读取消息无需自动回调timer-execute_callback()手动执行定时器回调逻辑替代Executor的自动执行sub-take()手动读取就绪的话题消息这是WaitSet模式下处理订阅者的核心gc-reset()守护条件触发后需手动重置否则下次wait()会直接判定为就绪。2.4 CMakeLists配置使用WaitSet需确保链接rclcpp库示例CMakeLists.txtcmake_minimum_required(VERSION 3.8) project(waitset_demo) if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES Clang) add_compile_options(-Wall -Wextra -Wpedantic) endif() find_package(ament_cmake REQUIRED) find_package(rclcpp REQUIRED) find_package(std_msgs REQUIRED) add_executable(waitset_demo_node src/waitset_demo.cpp) ament_target_dependencies(waitset_demo_node rclcpp std_msgs) install(TARGETS waitset_demo_node DESTINATION lib/${PROJECT_NAME}) if(BUILD_TESTING) find_package(ament_lint_auto REQUIRED) ament_lint_auto_find_test_dependencies() endif() ament_package()三、高级特性与注意事项3.1 线程安全WaitSet的所有方法add_xxx()、wait()、clear()非线程安全多线程操作时必须加锁std::mutex waitset_mutex;// 多线程添加实体时std::lock_guardstd::mutexlock(waitset_mutex);wait_set.add_subscription(new_sub);就绪实体的处理逻辑也需保证线程安全如多线程读取话题消息时加锁。3.2 实体生命周期管理若添加到WaitSet的实体如订阅者、定时器被销毁如reset()调用wait()会返回WaitResult::Error建议在实体销毁前先调用remove_xxx()从WaitSet中移除避免出错。3.3 实时性优化优先级处理在wait()返回后可按业务优先级处理就绪实体如先处理运动控制话题再处理日志话题超时时间调优实时场景建议设置短超时如10ms避免阻塞过久非实时场景可设为无限等待避免冗余监听仅添加需要监听的实体减少WaitSet的遍历开销。3.4 QoS与WaitSet的交互对于“可靠传输Reliable”的话题若消息未被确认WaitSet会持续将订阅者标记为就绪直到take()读取消息对于“历史记录History”QoSWaitSet会监听历史消息需注意take()的调用次数QoS事件如消息丢失、匹配状态变化可通过Event实体添加到WaitSet监听。3.5 与Executor的协同使用WaitSet可与Executor共存比如Executor管理大部分普通回调WaitSet单独处理实时性要求高的实体禁止将同一实体同时添加到Executor和WaitSet会导致回调被重复处理或实体状态混乱。四、典型应用场景机器人实时运动控制等待“运动指令话题”和“安全停止守护条件”优先处理安全停止逻辑嵌入式机器人开发精简Executor开销手动管理WaitSet以降低CPU占用多传感器数据同步等待激光雷达、相机、IMU的话题消息同步读取并融合数据服务/客户端交互等待服务请求/响应手动控制超时和重试逻辑。总结心定位*WaitSet是ROS2底层的同步等待原语封装DDS WaitSet用于手动监听Waitable实体的就绪状态是Executor的实现基础流程初始化WaitSet→添加实体→调用wait()等待→检查就绪实体→手动处理如take()读消息、执行回调→循环注意事项保证线程安全、管理实体生命周期、按业务优先级处理就绪实体实时场景需调优超时时间。