C++ 第 33 课:事件循环——spin() 到底在干什么

上一课标准答案:

  1. MyNode 是 C++ 类,不是运行时对象。真正的对象要等你写:

    auto node = std::make_shared();

之后才创建出来。因为 MyNode 继承了 rclcpp::Node,这个对象同时具备 ROS2 Node 的能力。

复制代码
auto node = std::make_shared<MyNode>();

这里的 node 是一个:

复制代码
std::shared_ptr<MyNode>

也就是管理 MyNode 对象的智能指针。可以画成:

复制代码
node
 │
 ▼
MyNode对象
├── Node父类部分
└── MyNode自己的部分
复制代码
rclcpp::init(argc, argv);

是【ROS2 新知识】,作用是:

初始化 ROS2 C++ 运行环境,让后面的 Node、Publisher、Subscriber 等 ROS2 功能可以工作。

复制代码
rclcpp::spin(node);

不能理解成"什么也不干地卡住"。更准确地说:

让 ROS2 持续处理这个 Node 的事件。

包括消息、Timer、Service、Action 等事件。

  1. 如果没有 spin(),程序创建完节点后通常会一路执行到结束,Subscriber 没有持续处理事件的机会,所以即使消息到了,callback 也可能根本不会被执行。

第 33 课:事件循环------spin() 到底在干什么

这节非常关键,因为它决定你是否真正理解 ROS2 的运行方式。

先把两个概念分开:

复制代码
【旧知识】
普通C++代码通常顺序执行

【ROS2新知识】
ROS2大量采用"事件驱动"

1. 普通 C++ 是什么感觉?

例如:

复制代码
int main()
{
    std::cout << 1 << std::endl;
    std::cout << 2 << std::endl;
    std::cout << 3 << std::endl;

    return 0;
}

执行顺序:

复制代码
1
↓
2
↓
3
↓
程序结束

非常明确:

我写哪一句,CPU 就按顺序执行哪一句。


2. 但机器人程序不能只按一次顺序跑完

假设一个机械臂控制节点需要等待:

复制代码
VLA动作
关节状态
相机数据
Timer
急停消息

问题是:

这些东西什么时候来?

不知道。

例如:

复制代码
0.2秒后可能有JointState
1秒后可能有相机图像
1.3秒后VLA才输出动作

所以程序不能这样:

复制代码
getVLAAction();
getJointState();
getCameraImage();

因为你不知道这些数据什么时候准备好。

ROS2 更适合:

我先注册好"事件来了该怎么办",然后持续等待。


3. 什么叫"事件"?

事件可以非常简单地理解成:

某件值得程序处理的事情发生了。

例如:

复制代码
新Topic消息到达
Timer到时间
Service请求到达
Action请求到达

这些都叫事件。


4. callback 是"事件发生后的处理办法"

例如我们提前写:

复制代码
void callback(const Msg::SharedPtr msg)
{
    std::cout << msg->data;
}

意思只是:

如果消息来了,我想这么处理。

注意:

定义 callback ≠ callback 正在执行。

这和你定义普通函数一样:

复制代码
void test()
{
}

只有被调用时它才执行。


5. 那到底谁调用 callback?

这正是:

复制代码
rclcpp::spin(node);

背后的核心机制之一。

可以粗略理解为:

复制代码
spin开始
  ↓
检查有没有事件
  ↓
没有?
  ↓
继续等待
  ↓
有事件?
  ↓
找到对应callback
  ↓
执行callback
  ↓
执行完
  ↓
继续等待下一个事件

这就是:

事件循环 Event Loop


6. 你可以把 spin() 想成前台值班员

假设一个 Node 是机器人控制中心。

它已经登记了:

复制代码
收到JointState → 调 jointCallback

收到VLA Action → 调 actionCallback

Timer到时间 → 调 timerCallback

然后:

复制代码
rclcpp::spin(node);

相当于:

"从现在开始值班,有事情发生就按登记表处理。"

于是:

复制代码
spin
│
├─ /joint_states来了
│      ↓
│   jointCallback()
│
├─ /vla_action来了
│      ↓
│   actionCallback()
│
├─ Timer到时间
│      ↓
│   timerCallback()
│
└─ 继续值班......

7. 为什么 callback 看起来是"自动执行"?

严格来说,不是 callback 有魔法。

真正发生的是:

复制代码
你提前把callback注册给ROS2
↓
spin持续等待事件
↓
事件发生
↓
ROS2执行器发现事件
↓
调用你之前注册的callback

所以:

callback 并不是"自己执行",而是 ROS2 在事件发生后替你调用。

这个理解非常重要。


8. 把 Subscriber 放进来

假设:

复制代码
Subscriber监听:
/number

注册 callback:

复制代码
void numberCallback(
    const std_msgs::msg::Int32::SharedPtr msg)
{
    std::cout << msg->data;
}

然后:

复制代码
rclcpp::spin(node);

程序进入持续事件处理状态。

此时 Publisher 发:

复制代码
data = 100

整个过程:

复制代码
Publisher
   ↓
/number
   ↓
新消息进入ROS2
   ↓
spin正在处理事件
   ↓
发现Subscriber有新消息
   ↓
调用numberCallback(msg)
   ↓
msg->data
   ↓
100
   ↓
callback结束
   ↓
spin继续等待

这就是 ROS2 Topic 通信真正"跑起来"的方式。


9. spin() 为什么通常一直不返回?

看:

复制代码
rclcpp::spin(node);

rclcpp::shutdown();

一般运行 ROS2 节点时,你会发现程序长期停在:

复制代码
spin(node);

不是程序坏了。

而是:

它正在持续值班。

正常情况下可能一直:

复制代码
等待
处理
等待
处理
......

直到你终止节点,例如:

复制代码
Ctrl + C

然后 spin() 才退出,再往下:

复制代码
rclcpp::shutdown();

10. spin() 和 while(true) 有点像,但不要完全等同

你以前学过:

复制代码
while (true)
{
}

它会一直循环。

所以从直觉上:

复制代码
rclcpp::spin(node);

确实有一点像:

复制代码
while (ROS2还在运行)
{
    等待事件;
    处理事件;
}

但 ROS2 的真实内部机制比这个复杂。

现阶段这样理解足够:

spin() = ROS2 的持续事件处理循环。


11. Node、Subscriber、callback、spin 各负责什么?

这四个必须分清:

概念 作用
Node ROS2 中工作的程序单元
Subscriber 监听某个 Topic
callback 消息来了以后具体怎么处理
spin 持续等待并调度这些事件

所以不是:

复制代码
Subscriber一直自己跑

而是更接近:

复制代码
Node里面有Subscriber
↓
spin让Node持续处理事件
↓
Subscriber消息到达
↓
callback执行

12. VLA 场景下一下就很清楚了

以后你的 ROS2 控制节点可能:

复制代码
VLAController Node
│
├── Subscriber:/vla_action
│      ↓
│   actionCallback()
│
├── Subscriber:/joint_states
│      ↓
│   jointStateCallback()
│
└── Publisher:/joint_command

程序:

复制代码
rclcpp::spin(node);

以后:

JointState 来了

复制代码
/joint_states
↓
jointStateCallback()
↓
更新当前机械臂状态

VLA Action 来了

复制代码
/vla_action
↓
actionCallback()
↓
读取目标动作
↓
发布机器人控制命令

然后:

复制代码
继续spin

所以这是一个长期在线的机器人程序。


13. 为什么机器人软件特别适合这种结构?

因为机器人世界本来就是异步的:

复制代码
相机不会等VLA
VLA不会精确每10ms输出一次
机械臂反馈不断到来
传感器各有自己的频率

如果所有东西都强行顺序执行:

复制代码
等相机
↓
等VLA
↓
等机械臂
↓
再等相机

程序会非常笨重。

ROS2 用事件机制,可以让:

复制代码
谁的数据来了
↓
就处理谁

这也是你后面理解整个机器人软件架构的基础。


14. 再看完整 main()

复制代码
int main(int argc, char** argv)
{
    rclcpp::init(argc, argv);

    auto node =
        std::make_shared<MyNode>();

    rclcpp::spin(node);

    rclcpp::shutdown();

    return 0;
}

现在每一行都可以翻译:

复制代码
main()
程序入口

↓

rclcpp::init(...)
初始化ROS2环境

↓

make_shared<MyNode>()
创建MyNode节点对象

↓

spin(node)
让节点持续等待并处理ROS2事件

↓

shutdown()
退出ROS2

↓

return 0
程序正常结束

这就是一个 ROS2 节点程序最基本的生命周期。


15. 一个容易混淆的问题:Publisher 也需要 spin 吗?

分情况。

如果 Publisher 只是:

复制代码
创建
↓
发一次
↓
结束

理论上不一定非要长期 spin。

但是典型 ROS2 节点通常还会有:

复制代码
Timer
Subscriber
Service
Action

这些都依赖持续事件处理。

例如"每秒发布一次"的 Publisher,通常会使用:

复制代码
Timer
↓
spin
↓
Timer到时间
↓
callback
↓
publish

所以实际 ROS2 程序里:

复制代码
spin(node);

非常常见。


本课压缩成一句话

spin(node) 让 ROS2 节点进入持续事件处理状态;消息、Timer、Service 等事件发生后,ROS2 调用对应 callback,处理完后继续等待。


本课小练习

1

callback 为什么不是自己"自动运行"的?

2

spin(node) 最核心的作用是什么?

3

消息处理完成以后,spin() 会直接结束程序吗?

4

下面流程补全:

复制代码
Publisher
↓
Topic
↓
Subscriber
↓
______
↓
处理消息

5

VLA Controller 同时订阅:

复制代码
/vla_action
/joint_states

如果 /joint_states 先来了,ROS2 应该优先发生什么?

相关推荐
野生技术架构师1 小时前
牛客网互联网大厂 Java 高频面试题整理(持续更新)
java·开发语言
殷色玫瑰1 小时前
AVL树:从平衡因子到四种旋转的完整实现
c语言·数据结构·c++·算法·visualstudio
小师兄吃牛肉2 小时前
Java语法 | 多重循环
java·开发语言·python
进化中的刀盾兽2 小时前
从零实现 C++ AI 大模型接入 SDK(九):LLMManager 统一模型管理与请求路由
c++
会周易的程序员2 小时前
STVM OSAL 层架构设计文档
c++·物联网·嵌入式·虚拟机·iot·软plc·iec61131
Han.miracle2 小时前
MySQL InnoDB:MVCC、记录锁、间隙锁、二级索引MVCC底层原理
java·开发语言
.YM.Z2 小时前
C++——【红黑树】原理详解:定义、性质、插入变色与旋转实现
开发语言·c++
jidaowansui2 小时前
B3643 图的存储———>数组模拟邻接矩阵
算法
y1su2 小时前
Leetcode 二分模板
java·数据结构·算法·leetcode·排序算法