C++ 第 38 课:线程、SingleThreadedExecutor、MultiThreadedExecutor、Callback Group

上一课答案

1. 真正负责调度 callback 的是谁?

是:

复制代码
Executor

spin() 更像是:

让 Executor 持续开始工作。


2. rclcpp::spin(node) 和 Executor 的关系

可以粗略理解:

复制代码
rclcpp::spin(node)
↓
ROS2帮你建立/使用Executor
↓
把node交给它
↓
Executor持续等待事件
↓
执行ready的callback

3. Publisher 是不是主要靠 Executor 调度发送?

不是。

例如:

复制代码
publisher_->publish(msg);

这句被执行时,就主动调用 Publisher 发布消息。

Publisher 本身不像 Subscriber / Timer 那样主要依赖一个"callback ready 后等待 Executor 执行"的模式。

但:

publish() 经常是在某个 callback 内部被调用。


4. 单线程 Executor 中,VLA callback 耗时 2 秒怎么办?

假设:

复制代码
VLA callback
耗时2秒

同时 Timer:

复制代码
每10ms ready一次

如果是单线程 Executor:

复制代码
VLA callback执行
↓
唯一线程被占用2秒
↓
Timer callback无法及时执行
↓
只能等待

所以控制周期会严重延迟。


5. Executor 能不能管理多个 Node?

可以。

例如:

复制代码
Executor
├── CameraNode
├── VLANode
└── RobotControllerNode

Executor 可以统一调度多个节点中的 callback。


第 38 课:线程、SingleThreadedExecutor、MultiThreadedExecutor、Callback Group

这一课同时补 C++ 并发基础 + ROS2 并发模型。


1. 先学一个新的 C++ 概念:线程 Thread

这是【C++ 新知识】。

你以前可以把程序想成:

复制代码
第1句
↓
第2句
↓
第3句

这相当于:

一条执行路线。

这条执行路线就可以粗略理解成一个:

复制代码
Thread
线程

例如:

复制代码
void taskA()
{
    // 做A
}

void taskB()
{
    // 做B
}

如果只有一个线程:

复制代码
taskA
↓
执行完
↓
taskB

不能真正同时执行。


2. 多线程是什么?

假设两个线程:

复制代码
线程1 → taskA

线程2 → taskB

那么在多核 CPU 上,它们可能真正并行运行。

例如:

复制代码
线程1:
VLA推理

线程2:
机械臂控制

这正是机器人系统为什么需要并发。


3. SingleThreadedExecutor

ROS2:

复制代码
SingleThreadedExecutor

意味着:

Executor 使用一个主要执行线程处理 callback。

假设:

复制代码
Callback A:VLA推理,2000ms
Callback B:控制循环,10ms

执行:

复制代码
A开始
████████████████ 2秒
A结束

B才能开始

于是 B 被卡住。


4. MultiThreadedExecutor

如果:

复制代码
MultiThreadedExecutor

有多个工作线程。

可能变成:

复制代码
线程1:
████████ VLA callback ████████

线程2:
control
control
control
control
...

这样:

VLA 推理不会必然完全阻塞控制 callback。


5. 但"多线程"立刻带来一个新问题

假设两个 callback 都访问:

复制代码
double target_angle;

Callback A:

复制代码
target_angle = 30.0;

Callback B:

复制代码
double command = target_angle;

如果同时发生:

复制代码
线程1正在写 target_angle

线程2正在读 target_angle

就可能发生:

数据竞争 Data Race

这是 C++ 并发里极其重要的概念。


6. 什么叫数据竞争?

假设:

复制代码
int x = 0;

线程 A:

复制代码
x = 100;

线程 B:

复制代码
std::cout << x;

如果两个线程没有任何同步机制,同时操作:

复制代码
同一个变量
+
至少一个线程写

就可能出现数据竞争。

结果可能不可预测。


7. ROS2 里这为什么非常现实?

未来你可能有:

复制代码
actionCallback
↓
修改 target_joint_

controlCallback
↓
读取 target_joint_

如果 MultiThreadedExecutor 同时执行:

复制代码
线程1
actionCallback()
修改目标

线程2
controlCallback()
读取目标

那么共享数据:

复制代码
target_joint_

就必须认真处理线程安全。


8. 所以不能简单认为"多线程更快更好"

单线程:

复制代码
优点:
简单
没有大量并发问题

缺点:
一个callback太慢会阻塞其他callback

多线程:

复制代码
优点:
不同callback可能并发

缺点:
共享数据
竞态
锁
线程安全
调试难度

所以工程设计不是:

"全部改 MultiThreadedExecutor 就完事。"


9. ROS2 引入 Callback Group

这是【ROS2 新知识】。

Callback Group 可以理解成:

给 callback 分组,并告诉 Executor 哪些 callback 能不能同时执行。

最重要的两类:

复制代码
MutuallyExclusive
Reentrant

10. MutuallyExclusive

意思:

同一个 group 里的 callback 不允许同时执行。

比如:

复制代码
Callback Group A
├── jointStateCallback
└── controlCallback

设置为:

复制代码
MutuallyExclusive

那么即使使用多线程 Executor:

复制代码
jointStateCallback
和
controlCallback

也不会同时执行。


11. 为什么有用?

因为它们可能共同访问:

复制代码
joint_state_

你不希望一个正在修改,一个正在读取。

所以:

复制代码
互斥执行

可以降低并发风险。


12. Reentrant

意思:

这个 group 里的 callback 可以并发执行,甚至同一种 callback 也可能同时被调用。

适合:

本身就是线程安全、互不影响的任务。

这个比 MutuallyExclusive 更需要你理解线程安全。


13. 所以 Executor 和 Callback Group 是不同层级

可以画:

复制代码
MultiThreadedExecutor
        │
        ▼
      Node
        │
   ┌────┴────┐
   │         │
Group A    Group B
   │         │
callback   callback

Executor:

负责调度。

Callback Group:

限制哪些 callback 可以并发。


14. VLA 场景怎么设计?

例如:

复制代码
RobotController Node
│
├── Group A:MutuallyExclusive
│   ├── jointStateCallback
│   └── controlTimerCallback
│
└── Group B
    └── vlaCallback

概念目标:

复制代码
控制和状态更新
→ 尽量保证一致性

VLA推理
→ 不要长时间堵住控制循环

但真实系统里,VLA 大模型通常还会进一步:

独立进程 / 独立 Node / Python 节点 / 推理服务器

而不是简单塞进一个 callback 里跑几秒。


15. 为什么更推荐把 VLA 和控制拆成不同 Node?

例如:

复制代码
VLANode
Python + PyTorch
负责:
视觉
语言
模型推理
动作生成

和:

复制代码
RobotController
C++ + ROS2
负责:
高频状态
安全检查
轨迹下发
控制

中间:

复制代码
Topic / Action

通信。

这就是 VLA 系统非常合理的一种架构:

复制代码
Python VLA
↓
低频高级动作
↓
ROS2通信
↓
C++控制节点
↓
高频可靠控制

而不是:

复制代码
C++控制callback里面
直接跑20亿参数模型

16. C++真正的多线程以后怎么写?

你以后会学:

复制代码
#include <thread>

std::thread t(task);

以及:

复制代码
std::mutex
std::lock_guard
std::condition_variable
std::atomic

这些不会只作为 ROS2 附属概念。

我们会专门系统学习:

复制代码
线程
共享内存
锁
死锁
数据竞争
原子变量
条件变量
线程生命周期

因为这本身就是现代 C++ 的重要部分。


17. 当前理解层级

现在先形成:

复制代码
程序
↓
可以有多个线程

Executor
↓
用线程执行ROS2 callback

SingleThreadedExecutor
↓
基本一个执行线程

MultiThreadedExecutor
↓
允许多个callback并发

Callback Group
↓
进一步约束哪些callback允许并发

共享数据
↓
必须考虑线程安全

这条链非常重要。


18. VLA 实际案例

假设:

复制代码
VLA输出周期:
500ms

机械臂控制周期:
10ms

如果两者强行单线程:

复制代码
VLA执行500ms
↓
控制循环被卡500ms

对机械臂几乎不可接受。

合理架构更接近:

复制代码
VLA Node
500ms一次高级动作
      │
      ▼
   ROS2 Topic
      │
      ▼
RobotController
10ms控制循环

两边不同运行节奏。

所以:

ROS2 不只是通信工具,它还帮助你把不同时间尺度的机器人功能拆开。


本课小练习

  1. SingleThreadedExecutor 为什么容易被长时间 callback 阻塞?

  2. MultiThreadedExecutor 最大的优势和最大的风险分别是什么?

  3. 两个线程同时操作同一个变量,并且至少一个线程写,可能产生什么问题?

  4. MutuallyExclusive Callback Group 主要限制什么?

  5. 如果 VLA 推理 500ms,而机械臂控制需要 10ms 一次,你更倾向把它们放在同一个阻塞 callback,还是拆开?为什么?

相关推荐
(Charon)1 小时前
【C++面试】互斥锁与读写锁:使用场景、实现原理与写饥饿问题
c++·算法
longlongzihan1 小时前
回溯法详解:LeetCode 46. 全排列
算法·leetcode·回溯
秋名RG1 小时前
时间复杂度、空间复杂度与算法稳定性详解
数据结构·算法·排序算法
白色的北极熊1 小时前
c语言 求余运算符
c语言·数据结构·算法
外收内放1 小时前
Python基础语法练习题(43-44)
开发语言·python
坤坤子吖1 小时前
Python基础语法学习:列表和元组
开发语言·笔记·python·学习
可乐鸡翅yeah_1 小时前
新手梳理:HLS 线上问题,哪些该提给 CDN,哪些该找后端切片服务
开发语言·前端·javascript·vue.js·网络协议·http·m3u8在线
波尔德1 小时前
Origin 2024 绘制钟形正态分布曲线:填充颜色并导出透明 PNG
人工智能·算法
Omics Pro2 小时前
微软:多模态生物世界模型
数据库·人工智能·算法·microsoft·机器学习·自然语言处理