4、ROS2 Executor深入:SingleThreadedExecutor、MultiThreadedExecutor、静态单线程执行器
说到ROS2的Executor,我刚开始接触时也一头雾水。说白了,它就是ROS2的「调度引擎」——决定你的回调函数什么时候跑、怎么跑。很多新手把节点写好了,却发现回调不执行、或者执行顺序乱套,十有八九是Executor没选对。
我个人习惯把Executor比作「餐厅后厨的排单系统」。你想想看,客人点单(消息到达),厨师(回调函数)得按什么顺序做菜?是一个厨师做完一道再做下一道,还是多个厨师同时开工?这就是我们今天要聊的核心。
4.1 SingleThreadedExecutor:单线程的「老实人」
这是ROS2的默认执行器。你创建一个节点,如果不指定,用的就是它。
工作原理:一个线程,一个接一个地处理所有回调。就像只有一个厨师,按顺序做菜。
核心特点:
- 所有回调在同一个线程中执行
- 回调之间天然互斥,不需要加锁
- 一个回调卡住,后面的全部排队等着
我在项目中遇到过这样一个坑:一个订阅回调里做了耗时3秒的传感器数据处理,结果控制指令的回调就一直等在那里。机器人反应慢半拍,差点撞墙。嗯,这里要注意——单线程执行器不适合有耗时操作的场景。
// 典型用法
rclcpp::executors::SingleThreadedExecutor executor;
auto node = std::make_shared<MyNode>();
executor.add_node(node);
executor.spin(); // 阻塞在这里,单线程处理所有回调
我的建议:如果你的回调都很轻量(微秒级),用单线程就够了。它简单、安全、没有竞态问题。
4.2 MultiThreadedExecutor:多线程的「效率狂」
当你的节点里有多个耗时回调,或者需要并行处理时,就该请出MultiThreadedExecutor了。
工作原理:内部维护一个线程池,多个回调可以同时执行。就像后厨有多个厨师,各做各的菜。
核心参数:
number_of_threads:线程数量,默认是CPU核心数- 每个线程独立从就绪队列中取回调执行
// 多线程用法
rclcpp::executors::MultiThreadedExecutor executor(
rclcpp::ExecutorOptions(),
4 // 4个线程
);
auto node = std::make_shared<MyNode>();
executor.add_node(node);
executor.spin();
我曾经在一个六轴机械臂项目里用过它。轨迹规划、力传感器读取、视觉反馈三个回调各占一个线程,互不干扰。但注意——多线程带来了并发,也带来了竞态。
避坑指南:我曾经因为没加锁,两个线程同时修改了同一个共享变量,导致机械臂关节角度跳变。从那以后,我养成了习惯:多线程环境下,所有共享数据都要用std::mutex保护,或者用ROS2的rclcpp::CallbackGroup做隔离。
4.3 静态单线程执行器:ROS2的「新秀」
这是ROS2 Humble版本引入的新执行器。说实话,我第一次看到这个名字也愣了一下——「静态」是什么意思?
工作原理:它在初始化阶段就把所有回调的调度顺序固定下来,运行时不再动态调度。就像后厨在营业前就把菜单排好序,厨师按固定顺序做菜。
与普通单线程的区别:
| 特性 | SingleThreadedExecutor | 静态单线程执行器 |
|---|---|---|
| 调度策略 | 动态轮询 | 静态预排序 |
| 确定性 | 低 | 高 |
| 适用场景 | 通用 | 实时控制 |
// 静态单线程用法
rclcpp::executors::StaticSingleThreadedExecutor executor;
auto node = std::make_shared<MyNode>();
executor.add_node(node);
executor.spin(); // 回调顺序在spin前已确定
为什么需要它?你想想看,在实时控制中,回调的执行顺序和时间确定性至关重要。普通单线程执行器每次轮询时,回调的就绪顺序可能不同,导致微秒级的抖动。而静态执行器把顺序固定了,抖动几乎为零。
我的经验:在电机控制频率达到1kHz以上时,我强烈推荐用静态单线程执行器。它牺牲了一点灵活性,换来了可预测的执行顺序。我曾经在无人机飞控项目中用它,控制周期抖动从±50微秒降到了±5微秒。
4.4 如何选择?一张表说清楚
| 场景 | 推荐执行器 | 原因 |
|---|---|---|
| 简单传感器读取 | SingleThreaded | 回调轻量,单线程足够 |
| 多传感器融合 | MultiThreaded | 各传感器回调可并行 |
| 实时控制(1kHz+) | 静态单线程 | 确定性高,抖动小 |
| 复杂状态机 | MultiThreaded | 状态转换和回调可分离 |
我个人习惯是:能用单线程就不用多线程,能用静态就不用动态。多线程带来的复杂度,往往比它解决的问题还多。除非你明确需要并行,否则老老实实用单线程。
4.5 实战中的小技巧
最后分享几个我在项目中积累的经验:
- 回调分组:用
rclcpp::CallbackGroup把不同优先级的回调分开。比如控制回调放一组,日志回调放另一组。 - 线程数设置:多线程执行器的线程数不是越多越好。我一般设为CPU核心数,或者核心数+1。
- 调试技巧:如果发现回调不执行,先检查Executor是否调用了
spin()。这个低级错误我犯过不止一次。
重要提醒:不要在回调里做阻塞操作,比如sleep()或等待锁。这会拖垮整个Executor。如果必须等待,考虑用异步方式或单独开线程。
好了,Executor这块就聊到这。说白了,选对执行器,你的ROS2节点就成功了一半。下一章我们聊聊行为树,那又是另一个有意思的话题了。