

🔥大奇个人主页 :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:传递成员函数的地址
- 参数2:传递实例化对象地址
- 参数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++ 其实分为三步:
- 把变量从内存读到 CPU 寄存器。
- 在寄存器里加 1。
- 把结果写回内存。
非原子操作的问题: 如果线程 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 的底层客户端库(rclcpp 或 rclpy)会自动接管这个信号,以确保机器人能够优雅地关闭,而不是直接"闪退"。
- ROS 2 是如何捕捉它的?
当你调用 rclcpp::spin(node) 或 rclpy.spin(node) 时,ROS 2 实际上是在后台运行一个循环。
- 捕捉机制 :ROS 2 注册了一个全局信号处理器。当你按下
Ctrl+C时,信号处理器会将全局状态标记为"停止" - 停止循环 :
rclcpp::ok()会从true变为false - 退出 Spin :
spin函数检测到状态改变,立即跳出循环,继续执行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;
});
