第3章:ROS2执行器机制:单线程、多线程与静态单线程执行器

聊到ROS2的执行器,很多朋友第一反应就是“不就是个循环调度吗?”
嗯,这么说也没错。但实际项目中,执行器选错了,系统直接卡死的情况,我见过太多次了。

今天咱们就把三种执行器掰开揉碎讲清楚。
单线程、多线程、静态单线程,它们到底有什么区别?什么时候该用哪个?

3.1 单线程执行器:最朴素,也最可靠

单线程执行器,说白了就是一个线程里跑所有回调。
你想想看,所有订阅、定时器、服务回调,全挤在一条流水线上。

工作原理

  • 内部维护一个就绪队列
  • 每次从队列头部取一个回调执行
  • 执行完再取下一个
// 伪代码示意
while (rclcpp::ok()) {
    auto callback = executor.get_next_ready_callback();
    callback.execute();
}

核心特点: 没有并发,没有锁竞争,执行顺序完全可控。

我在做一款工业机械臂控制器时,就吃过多线程的亏。后来换成单线程执行器,反而稳定了。
为什么?因为机械臂的控制周期是固定的,回调之间没有依赖冲突,单线程足够。

适用场景:

  • 回调数量少(10个以内)
  • 回调执行时间短且稳定
  • 对实时性要求不高,但要求确定性

注意: 如果某个回调阻塞了,整个执行器就卡住了。我曾经见过一个新手把sleep(100ms)写进回调里,结果整个系统响应延迟飙升到秒级。

3.2 多线程执行器:并发利器,但需谨慎

多线程执行器,就是开多个线程同时处理回调。
听起来很美好,对吧?但实际用起来,坑不少。

工作原理

  • 内部维护一个线程池
  • 每个线程独立从就绪队列取回调
  • 多个回调可以同时执行
// 创建多线程执行器
auto executor = std::make_shared<rclcpp::executors::MultiThreadedExecutor>(
    rclcpp::executor::ExecutorArgs(), 
    4  // 4个线程
);

我个人习惯,多线程执行器的线程数一般设为CPU核心数+1。
但这不是铁律。我记得有一次做自动驾驶感知模块,开了8个线程反而比4个线程慢。
为什么?因为线程切换开销超过了并行收益。

核心特点: 并发执行,提高吞吐量,但引入锁竞争和线程切换开销。

避坑指南:

  • 回调之间如果有共享数据,必须加锁
  • 不要依赖回调的执行顺序
  • 线程数不是越多越好

我曾经... 在一个项目中,两个回调同时访问同一个全局变量,没加锁。结果数据时而对时而错,排查了整整两天。从那以后,我只要用多线程执行器,第一件事就是检查共享数据保护。

3.3 静态单线程执行器:实时系统的首选

这个执行器是ROS2 Humble版本引入的,专门为实时系统设计。
它和普通单线程执行器有什么区别?

核心区别: 静态单线程执行器在初始化阶段就确定了回调的执行顺序,运行时不再动态调度。

// 创建静态单线程执行器
auto executor = std::make_shared<rclcpp::executors::StaticSingleThreadedExecutor>();

你想想看,普通单线程执行器每次都要从就绪队列里选一个回调,这个“选”的过程是有开销的。
静态单线程执行器直接把这个步骤省了,执行顺序在初始化时就定死了。

核心特点: 零动态调度开销,执行顺序完全确定,适合硬实时系统。

我在做无人机飞控系统时,就用了静态单线程执行器。
飞控的每个控制周期是1ms,任何额外的调度开销都可能导致炸机。
静态单线程执行器把调度延迟降到了微秒级,完全满足需求。

适用场景:

  • 硬实时系统(控制周期固定)
  • 回调执行时间已知且稳定
  • 不需要动态添加/删除节点

3.4 如何选择?一张表说清楚

特性 单线程 多线程 静态单线程
并发能力
调度开销 极低
执行顺序确定性 极高
锁竞争风险
适合场景 简单系统 高吞吐系统 实时系统

嗯,这里要注意一点:
静态单线程执行器虽然好,但它不支持运行时动态添加节点。
如果你的系统需要热插拔节点,那就只能用多线程执行器。

3.5 实战建议

说了这么多,我给大家一个实际项目的选择思路:

  1. 先评估你的回调:每个回调执行多久?有没有阻塞操作?
  2. 再评估你的实时性要求:是软实时还是硬实时?
  3. 最后做选择

我的经验法则:

  • 回调少于10个,执行时间<1ms → 单线程执行器
  • 回调多且独立,需要高吞吐 → 多线程执行器
  • 控制周期固定,延迟要求<100μs → 静态单线程执行器

记住,没有银弹。
每种执行器都有它的脾气,选对了事半功倍,选错了...嗯,你会花很多时间调试。

下一章,咱们聊聊回调组的配置,那是多线程执行器性能调优的关键。