【C++】ROS2捕获Ctrl+C信号+原子操作与线程生命周期控制

🔥大奇个人主页 :https://blog.csdn.net/m0_75192474?type=blog

本文所属专栏:https://blog.csdn.net/m0_75192474/category_13131150.html

使用Boost的thread创建子线程

boost::thread的构造函数有许多个重载,下面介绍常用的两个

  • 重载1:thread<F, A1, A2>(F f, A1 a1, A2 a2)
cpp 复制代码
boost::thread t(&UdpServer_Esp32::Receive_fromEsp32S3,&udpServer,boost::ref(ifKeepRunning));
  1. 参数1:传递成员函数的地址
  2. 参数2:传递实例化对象地址
  3. 参数3 :传递成员函数的参数的引用
  • 重载2:template <class F> explicit thread(F f);

F可以作为函数指针,Lambda表达式等,以Lambda表达式为例

cpp 复制代码
    boost::thread receive_thread([&udpServer]() {
        try {
            udpServer.Receive_fromEsp32S3(ifKeepRunning);  // 子线程函数
            printf("子线程退出!\\\\n");
        }
        catch (...) {
            printf("子线程崩溃!\\\\n");
            system("pause");
        }
    });
    //初始

Lambda表达式格式

组成部分 名称 作用 例子
[ ] 捕获列表 决定外部变量如何进入 Lambda 内部 [&] (引用), [=] (拷贝)
( ) 参数列表 像普通函数一样接收输入参数 (int a, int b)
-> 返回类型 指定函数返回什么(通常可省略,由编译器自动推导) -> int
{ } 函数体 具体要执行的代码逻辑 { return a + b; }

捕获列表:

  • []:不捕获任何变量。
  • [&]隐式引用捕获 。Lambda 内部用到的所有外部变量都按引用处理(不拷贝,直接操作原变量)
  • [=]隐式拷贝捕获。所有变量都复制一份副本进去(子线程改了不影响主线程)
  • [&udpServer]显式引用捕获 。只把 udpServer 按引用传进去,别的不用
  • [this] :捕获当前类的 this 指针(在类成员函数内部定义 Lambda 时必用)

线程中join()detch()区别

  • join() ------ 阻塞等待

当调用 join() 时,主线程会停在原地,直到该子线程执行完毕。

子线程执行完后,主线程会负责回收子线程的系统资源(如栈内存、线程描述符)

  • detach() ------ 分离运行

当调用 detach() 时,子线程与主线程脱离关系,在后台独立运行

行为异步执行线程不再管子线程,直接往下跑。

清理:子线程变成"守护线程",当它运行结束时,由操作系统(OS)自动回收资源。

原子操作

原子操作在多线程编程中可以保证线程的安全 ,读取和写入操作要么**"全做"**,要么"全不做",不会出现读到一半的情况

  • 原子性:读取和写入操作要么"全做",要么"全不做",不会出现读到一半的情况。
  • 内存可见性 :一旦主线程修改了它,所有其他 CPU 核心上的子线程会立即看到最新值。它强制 CPU 刷新缓存,直接去内存里看。
  • 禁止指令重排 :编译器和 CPU 有时为了优化会乱动代码顺序,atomic防止代码逻辑被优化乱了。

在计算机底层,一个简单的 C++ 语句 count++ 其实分为三步:

  1. 把变量从内存读到 CPU 寄存器。
  2. 在寄存器里加 1。
  3. 把结果写回内存。

非原子操作的问题: 如果线程 A 刚执行完第二步(加了1),还没来得及写回内存,线程 B 就插进来读了内存(读到的还是旧值)。等两个线程都跑完,原本应该加 2 的变量,实际上只加了 1。这就是经典的竞态条件,即一个线程在写入时。另一个线程过来读取。

在c++中使用原子操作

cpp 复制代码
#include <atomic>

使用原子变量 ifKeepRunning 来通过主线程控制子线程的终止

cpp 复制代码
std::atomic<bool>ifKeepRunning = true;

启动一个子线程并将这个标志传入参数中

cpp 复制代码
    boost::thread receive_thread([&udpServer]() {
        try {
            udpServer.Receive_fromEsp32S3(ifKeepRunning);  // 子线程函数
            printf("子线程退出!\\\\n");
        }
        catch (...) {
            printf("子线程崩溃!\\\\n");
            system("pause");
        }
    });

子线程

cpp 复制代码
void UdpServer_Esp32::Receive_fromEsp32S3(std::atomic<bool> &ifrunning)
{
  while (ifrunning)
    {...}
 }

主线程可以添加一个死循环,并在适当位置退出

cpp 复制代码
while(1){
		if(...){
		ifKeepRunning = false;
		break;
		}
}

此时子线程就会立即相应这个标志位被置为假,从而退出子线程

如果使用普通的bool标志位,会出现哪些问题?

  • 脏读 :主线程把 ifKeepRunning 改成了 false,但子线程的 CPU 核心可能为了快,直接从自己的缓存里读数据,没看见主线程的修改,导致子线程无法停止
  • 竞态条件:如果两个线程同时尝试修改同一个变量,可能会导致内存冲突,程序直接崩溃

和互斥锁有什么区别?

  • 无锁 :相比于 std::mutex(互斥锁),原子操作通常直接利用 CPU 的硬件指令,它不需要让线程进入"休眠"和"唤醒"状态,所以性能极高
  • 不会死锁:锁用不好会死锁,原子操作永远不会死锁
特性 原子操作 (std::atomic) 互斥锁 (std::mutex)
颗粒度 极细(仅限单个变量) (可以保护一整段代码)
开销 极低(硬件级指令) 较高(涉及操作系统上下文切换)
复杂逻辑 难以处理(如:同时修改三个变量) 易于处理
类比 自动贩卖机(一次只能进一个硬币) 带锁的更衣室(进去后反锁,干完再出来)

ROS2捕捉Ctrl+C信号

在 Linux 系统中,按下 Ctrl+C 会向进程发送一个 SIGINT(中断)信号。ROS 2 的底层客户端库(rclcpprclpy)会自动接管这个信号,以确保机器人能够优雅地关闭,而不是直接"闪退"。

  • ROS 2 是如何捕捉它的?

当你调用 rclcpp::spin(node)rclpy.spin(node) 时,ROS 2 实际上是在后台运行一个循环。

  • 捕捉机制 :ROS 2 注册了一个全局信号处理器。当你按下 Ctrl+C 时,信号处理器会将全局状态标记为"停止"
  • 停止循环rclcpp::ok() 会从 true 变为 false
  • 退出 Spinspin 函数检测到状态改变,立即跳出循环,继续执行 main 函数后面的清理代码

重写Ctrl+c信号让子线程优雅退出

这个launch文件中有两个节点,

  • 一个节点udptoros_node-1是负责与小车udp通信并发布原始数据话题的线程,其中udp通信部分不是用ROS的而是 一个线程;
  • 另一个节点rawSubaimPub_node-2 是负责将原始数据打包成ros标准话题格式发布的节点是标准的ros::spin()

当我按下**Ctrl+C**后,终端的输出是这样的

此过程分为4个阶段

1. 信号接收确认

bash 复制代码
^C[WARNING] [launch]: user interrupted with ctrl-c (SIGINT)

当按下键盘ctrl+c,操作系统将 SIGINT 发送给 ros2 launch 进程。launch 系统第一时间捕捉到并发出告警。

2. 节点内部处理器的触发

bash 复制代码
[udptoros_node-1] [INFO] [...] [rclcpp]: signal_handler(SIGINT/SIGTERM)
[rawSubaimPub_node-2] [INFO] [...] [rclcpp]: signal_handler(SIGINT/SIGTERM)

这两个节点都打印出了 signal_handler

这是 rclcpp(ROS 2 的 C++ 客户端库)最核心的捕捉体现 。ROS 2 会在初始化时自动注册一个全局信号处理函数。它捕捉到信号后,会将 rclcpp::ok() 设为 false

3. 正常关闭与资源回收

bash 复制代码
[rawSubaimPub_node-2] process has finished cleanly [pid 4773]
[ERROR] [udptoros_node-1]: process[udptoros_node-1] failed to terminate '5' seconds after receiving 'SIGINT', escalating to 'SIGTERM'
[INFO] [udptoros_node-1]: sending signal 'SIGTERM' to process[udptoros_node-1]
[udptoros_node-1] [INFO] [1775303809.278800724] [rclcpp]: signal_handler(SIGINT/SIGTERM)

rawSubaimPub_node 显示"cleanly",说明该节点 被正常回收;但是 udptoros_node-1却出现报错

ROS 2 给每个节点 5 秒钟 的时间去自愿退出(SIGINT),如果超时, ROS 2 只能将信号升级为 SIGTERM(强制终止)来强行关掉它

4. 系统级别内核直接杀死节点

bash 复制代码
[ERROR] [udptoros_node-1]: process[udptoros_node-1] failed to terminate '10.0' seconds after receiving 'SIGTERM', escalating to 'SIGKILL'
[INFO] [udptoros_node-1]: sending signal 'SIGKILL' to process[udptoros_node-1]
[ERROR] [udptoros_node-1]: process has died [pid 5260, exit code -9, cmd '/home/rqtz/sros_ws/install/looraysbot_udptoros/lib/looraysbot_udptoros/udptoros_node --ros-args -r __node:=looraysbot_comm --params-file /home/rqtz/sros_ws/install/looraysbot_bringup/share/looraysbot_bringup/config/looraysBot.yaml'].

发送 SIGKILL。这是内核级别的强制删除,不给进程任何解释机会

原因分析:

造成udptoros_node-1 无法正常退出的原因有以下几个方面

  • 该节点中分为两个线程 :一个线程负责与小车进行UDP网络通信,非ROS线程,而主线程负责发送原始数据话题,是标准的ros::spin() ;由于这个UDP线程中,
cpp 复制代码
size_t recv_len = socket_.receive_from(
boost::asio::buffer(recv_buf_, 1024),
remote_endpoint_);

该函数式阻塞式的接收数据,如果小车 此时无法向pc端发送数据,那么子线程的循环就会一直卡在这里,无法退出,而主线程在ctrl+c退出后,该子线程是 join()方式,所以主线程就会一直等待这个子线程退出,才执行后面的代码

解决问题:

使用原子操作定义一个线程退出标志,当主线程退出时候,向子线程发送一个退出标志位,来管理子线程的死循环;同时对 receive_from 增加超时处理

cpp 复制代码
//定义原子变量
std::atomic<bool>ifKeepRunning = true;
//主线程
while (rclcpp::ok() && ifKeepRunning){...}
ifKeepRunning  = false; //退出标志位
receive_thread.join();//等待子线程退出
rclcpp::shutdown();//关闭ros循环
//子线程
void UdpServer_Esp32::Receive_fromEsp32S3(std::atomic<bool> &ifrunning){
	while (ifrunning){...}
}

以上执行流程就是,当按下ctrl+c后,主线程的ros循环先收到响应,然后rclcpp::ok()返回false主线程退出,紧接着线程退出标志位被置为false,并等待子线程退出,此时又因为是原子操作,可以通知其他子线程立即响应该标志位,子线程的while循环被退出,若此时socket_函数被阻塞,子线程这个循环还是没有办法执行到下一次判断这个标志位,所以添加超时时间,让循环进行,直到响应退出标志位ifKeepRunning 当子线程退出后,主线程也就安全退出了。

  • 或者直接手写捕捉ctrl+c信号,并在该函数中让ifKeepRunning = false
cpp 复制代码
#include <signal.h>
  // 捕捉ctrl+c信号
  signal(SIGINT,[](int){
      ifKeepRunning = false;
  });
相关推荐
老四啊laosi10 小时前
[C++进阶] 24. 哈希表封装unordered_map && unordered_set
c++·哈希表·封装·unordered_map·unordered_set
妙为11 小时前
银河麒麟V4下编译Qt5.12.12源码
c++·qt·国产化·osg3.6.5·osgearth3.2·银河麒麟v4
史迪仔011214 小时前
[QML] QML IMage图像处理
开发语言·前端·javascript·c++·qt
会编程的土豆15 小时前
【数据结构与算法】再次全面了解LCS底层
开发语言·数据结构·c++·算法
低频电磁之道15 小时前
解决 Windows C++ DLL 导出类不可见的编译错误
c++·windows
君义_noip17 小时前
信息学奥赛一本通 4150:【GESP2509七级】⾦币收集 | 洛谷 P14078 [GESP202509 七级] 金币收集
c++·算法·gesp·信息学奥赛·csp-s
Ricky_Theseus17 小时前
静态链接与动态链接
c++
澈20717 小时前
双指针,数组去重
c++·算法
小辉同志18 小时前
207. 课程表
c++·算法·力扣·图论