网站首页 > 基础教程 正文
之前我研究了机器人开发中的 ROS2(Jazzy)系统相关内容。并将官网中比较重要的教程和概念,按照自己的学习顺序翻译成了中文,并进行了整理和记录。我在记录的过程中,还针对一些不太好理解的部分进行了额外的研究和补充说明。到目前为止,已经整理了20多篇文章。如果你想回顾之前的内容,可以查阅主页中 ROS2(Jazzy)相关文章。
在研究 ROS2 的过程中,我发现它使用了不少 C++11 的新特性。这让我意识到,深入掌握这些特性对于深入理解 ROS2 的实现原理和优化代码非常重要。
因此,我萌生了撰写 C++11 系列文章的想法。目前已经完成了以下几篇文章:
- ROS2性能狂飙:C++11移动语义‘偷梁换柱’实战
- C++11 Lambda 表达式以及 std::function 和 std::bind
- C++11 智能指针:unique_ptr、shared_ptr和weak_ptr
而本文是第四篇,主要总结的是 C++11 的线程管理(std::thread)
C++11 的线程管理
C++11 引入了全面的多线程支持,使并发编程成为语言标准的一部分。核心组件包括线程管理(std::thread)、原子操作(std::atomic)和同步机制(互斥锁、条件变量)。
线程管理类 std::thread,用于创建和管理操作系统线程。它提供了跨平台的线程操作接口,消除了对平台特定 API(如 POSIX pthreads 或 Windows Threads)的依赖。
线程创建
我们可以通过传递可调用对象来构造 std::thread 线程实例,比如函数、Lambda、函数对象:
#include <thread>
// 函数形式
void print(int num, const std::string& str) {
std::cout << num << " " << str << "\n";
}
// Lambda 形式
auto lambda = [](float f) { /*...*/ };
int main() {
// 创建线程并立即执行
std::thread t1(print, 42, "Hello"); // 值传递
std::string msg = "World";
std::thread t2([&msg]() { // 引用捕获
std::cout << msg << "\n";
});
t1.join();
t2.join();
}
当我们构造 std::thread 实例时,可以使用以下几种数据传递机制:
- 默认行为:参数按值拷贝
- 传递引用:需使用 std::ref 包装
- 移动语义:使用 std::move 避免拷贝
void process_data(const BigData& data); // 大对象
BigData data;
std::thread t1(process_data, data); // 拷贝data(可能昂贵)
std::thread t2(process_data, std::ref(data)); // 传递引用
std::thread t3(process_data, std::move(data)); // 移动语义
另外,我们需要注意的线程异常隔离问题,因为线程内异常不会传播到主线程,所以我们必须在每个线程内部处理异常:
std::thread t([] {
try {
// 可能抛出异常的代码
} catch (...) {
// 处理所有异常
}
});
线程生命周期的管理
我们可以通过以下操作对线程的生命周期进行管理:
- 等待线程完成(join)
- 分离线程(detach)
- RAII 包装器(推荐模式)
在 std::thread 的生命周期管理中,"可连接"(joinable)和 "不可连接"(non-joinable)是核心概念,直接关系到线程的安全管理和资源回收。
1. 状态定义
- 可连接状态 表示线程对象关联着一个活跃的或已结束但未清理的执行线程。可连接状态的条件有:1)通过构造函数创建了新线程2)尚未调用 join() 或 detach()
- 不可连接状态 表示线程对象不再关联任何执行线程。不可连接状态的条件有:1)已调用 join()2)已调用 detach()3)被移动(所有权转移)4)默认构造(无关联线程)
2. 状态转换
a. 进入可连接状态
// 创建线程对象 → 可连接状态
std::thread t([]{ /* 执行任务 */ }); // joinable() == true
b. 转为不可连接状态
// 方式1: 调用 join()
t.join(); // joinable() == false
// 方式2: 调用 detach()
t.detach(); // joinable() == false
// 方式3: 移动所有权
std::thread t2 = std::move(t); // t变为不可连接,t2变为可连接
3. 使用规则与后果
1)析构时必须不可连接
{
std::thread t([]{ /*...*/ }); // 可连接状态
// 未调用 join/detach → 析构时终止程序!
} // 此处调用 std::terminate() 终止程序!
2)禁止重复操作
std::thread t([]{ /*...*/ });
t.join(); // 转为不可连接
t.join(); // 抛出 std::system_error
// 错误信息:"Invalid argument"
3)移动后的状态
std::thread t1([]{ /*...*/ }); // t1 可连接
std::thread t2 = std::move(t1); // t1 不可连接,t2 可连接
t1.joinable(); // false
t2.joinable(); // true
4. 状态检测:joinable()
joinable()主要用于安全判断当前状态,避免重复操作:
std::thread t;
if (t.joinable()) { // 检查状态
// 安全操作区域
t.join(); // 或 t.detach()
}
典型使用场景:
void safe_thread_management() {
std::thread t([]{ /*...*/ });
try {
// 可能抛出异常的代码
} catch (...) {
if (t.joinable()) t.join(); // 异常时安全清理
throw;
}
// 正常流程
if (t.joinable()) t.join();
}
5. 那么为什么需要这种设计?
首要原因是为了资源安全,操作系统线程是重要资源(通常占用 1-10 MB 内存),所以必须明确决定线程结束后的处理方式。
第二个原因是为了避免僵尸线程,join() 是为了确保线程资源被回收,而detach() 是为了明确的放弃所有权。
第三个原因是为了防止未定义行为,比如,未同步的线程访问已销毁的局部变量;再比如,未回收线程导致资源泄漏。
6. 正确用法示例
理解并正确管理线程的可连接状态,是避免多线程程序崩溃和资源泄漏的基础,也是编写健壮并发代码的关键所在。
下面列出几个正确使用的线程周期管理的场景:
场景1:等待线程完成(join)
void process_data() {
std::vector<int> results;
std::thread worker([&]{
results = calculate(); // 耗时计算
});
// 必须等待结果完成
worker.join(); // 阻塞直到计算完成
use_results(results); // 安全使用结果
}
场景2:分离后台线程(detach)
void start_background_task() {
std::thread([]{
while (true) {
// 周期性后台任务
std::this_thread::sleep_for(1h);
cleanup();
}
}).detach(); // 立即分离
}
场景3:RAII 自动管理
RAII(Resource Acquisition Is Initialization) 是 C++ 的核心设计理念,中文译为"资源获取即初始化"。这是一种利用对象生命周期来管理资源(内存、文件句柄、网络连接、线程等)的技术,确保资源在任何执行路径下都能被正确释放。
class ThreadRAII {
std::thread t;
enum class Action { Join, Detach };
Action action;
public:
ThreadRAII(std::thread&& t, Action a)
: t(std::move(t)), action(a) {}
~ThreadRAII() {
if (t.joinable()) {
if (action == Action::Join) t.join();
else t.detach();
}
}
// ... 禁止拷贝 ...
};
// 使用示例
{
ThreadRAII tr(
std::thread([]{ /* 关键任务 */ }),
ThreadRAII::Action::Join // 退出作用域自动join
);
// 即使此处抛出异常,线程也会被正确清理
}
总结
特性说明构造即启动线程在创建时立即开始执行移动语义线程所有权可转移,不可复制join/detach必须在线程析构前调用,否则程序终止参数传递默认值传递,引用需用std::ref,大对象用std::move异常隔离线程内异常不会传播到主线程RAII模式推荐使用包装类管理线程生命周期
正确使用std::thread需要注意:
- 始终管理线程生命周期(join/detach)
- 谨慎处理共享数据(使用同步原语)
- 避免线程间悬垂引用
- 考虑性能影响和线程创建开销
- 优先使用高级抽象(如std::async)简化代码
欢迎关注【智践行】一起学习机器人开发,发送【C++】获得学习资料。
猜你喜欢
- 2025-06-12 实例解析C++多线程并发---异步编程
- 2025-06-12 Go与Rust多线程编程深度对比(go和rust2021)
- 2025-06-12 Rust + Slint异步UI编程的奥秘,让你的应用永不假死!
- 2025-06-12 25道C++经典面试题详解,附全套学习资料免费领!
- 2025-06-12 Rust并发编程中的内部可变性(rust 并发)
- 2025-06-12 深入解析C++并发编程:从多线程到现代C++并发库
- 2025-06-12 C++ 创建新线程的核心指南:从基础到关键要点
- 2025-06-12 Rust 语言的借用规则:构筑安全内存管理体系的核心保障机制
- 2025-06-12 C++线程池的原理和方法实践(c线程池实现原理)
- 2025-06-12 你们在编程时遇到过什么离谱的bug吗?
- 最近发表
- 标签列表
-
- jsp (69)
- gitpush (78)
- gitreset (66)
- python字典 (67)
- dockercp (63)
- gitclone命令 (63)
- dockersave (62)
- linux命令大全 (65)
- pythonif (86)
- location.href (69)
- dockerexec (65)
- tail-f (79)
- queryselectorall (63)
- location.search (79)
- bootstrap教程 (74)
- deletesql (62)
- linuxgzip (68)
- 字符串连接 (73)
- html标签 (69)
- c++初始化列表 (64)
- mysqlinnodbmyisam区别 (63)
- arraylistadd (66)
- mysqldatesub函数 (63)
- window10java环境变量设置 (66)
- c++虚函数和纯虚函数的区别 (66)