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,还是拆开?为什么?

相关推荐
星夜离尘561 天前
MATLAB 工程实验:不等间隔流量积分,先把缺口与覆盖率说清楚
开发语言·matlab·时间戳·数据质量·数值积分
Ai-_Man1 天前
Meta Muse还是不可以导出左侧所有对话,只能点进去单条内容导出,方法不对吗?
开发语言·人工智能·ai·小程序·c#·1024程序员节
梓豪csd1 天前
消息订阅中心:谁该收到哪条通知
java·开发语言·人工智能·低代码·自动化·1024程序员节
会编程的小孩1 天前
C++ qt的安装
c++
学逆向的1 天前
C 枚举变量
c语言·开发语言·网络安全
Sakura_j_h_1 天前
Python 在定时任务里输出 emoji,UnicodeEncodeError 从哪来
开发语言·字符编码·windows·python·定时任务
企业数字化笔记1 天前
视频人体姿态怎么做三维可视化?2D关键点、深度局限与技术选型
人工智能·算法·音视频
?abc!1 天前
(数组)581. 最短无序连续子数组
数据结构·算法·leetcode
小成202303202651 天前
C++~07补充
开发语言·c++
小郎君6581 天前
MPTT树复选框的半选状态:一棵百万节点树从递归到计数器,再到区间覆盖
算法