2. 线程的创建与管理:std::thread的基本用法、线程的join与detach、线程参数的传递
好,咱们今天聊聊线程的创建和管理。说实话,这是并发编程里最基础、也最容易踩坑的地方。我刚开始用C++11的std::thread时,觉得这东西不就是new一个对象嘛,有啥难的?结果第一次上线就被坑了——线程还没跑完,主线程就退出了,程序直接崩溃。嗯,从那以后我再也不敢小看这几个API了。
2.1 std::thread的基本用法
std::thread是C++11引入的标准线程库。说白了,它就是把你一个函数(或可调用对象)扔到另一个执行流里去跑。创建线程的方式很简单:
#include <iostream>
#include <thread>
void hello() {
std::cout << "Hello from thread!" << std::endl;
}
int main() {
std::thread t(hello); // 创建线程,执行hello函数
t.join(); // 等待线程结束
return 0;
}
你看,就这么几行。但这里有个细节——std::thread一旦构造完成,线程就立刻开始执行了。它不像Java的Thread.start()那样需要手动启动。我个人习惯是:构造线程和调用join之间,尽量不要插入太多逻辑,免得忘记处理线程生命周期。
核心要点:
std::thread构造后线程立即启动- 可调用对象可以是函数、函数对象、lambda表达式
- 线程对象必须被
join或detach,否则析构时会调用std::terminate
2.2 线程的join与detach
这两个操作,是线程管理的核心。我见过不少新手在这上面翻车,咱们一个一个说。
2.2.1 join:等待线程结束
join()的意思是:当前线程(通常是主线程)阻塞在这里,等目标线程执行完了再继续往下走。你想想看,这就像你叫了个外卖,你等着外卖到了再吃饭——这就是join。
std::thread t(worker);
t.join(); // 主线程在这里等着,直到worker线程结束
// 到这里,worker线程肯定已经结束了
我在项目中遇到过一个问题:多个线程需要并行计算,最后汇总结果。这时候每个线程都要join,但顺序无所谓。我建议用vector存线程对象,然后统一join:
std::vector<std::thread> threads;
for (int i = 0; i < 4; ++i) {
threads.emplace_back(worker, i);
}
for (auto& t : threads) {
t.join(); // 逐个等待
}
2.2.2 detach:让线程自由
detach()正好相反——它把线程和std::thread对象分离。线程变成后台守护线程,自己跑自己的,你再也管不着它了。
std::thread t(background_task);
t.detach(); // 线程独立运行,t不再关联任何线程
// 注意:此时不能再调用t.join(),会抛异常
我曾经踩过的坑:
detach后的线程如果访问了主线程的局部变量,而主线程先退出了,那局部变量就被销毁了。线程还在跑,访问的是野指针。程序崩溃是小事,数据损坏才是大问题。所以,detach前一定要确保线程不再依赖主线程的局部资源。
2.2.3 joinable判断
一个线程要么被join,要么被detach,不能重复操作。你可以用joinable()来检查:
if (t.joinable()) {
t.join(); // 安全调用
}
这个检查很重要。我习惯在析构函数里做一次安全join,防止异常导致线程没被处理。
2.3 线程参数的传递
给线程传参,看起来简单,其实有不少门道。咱们直接看代码:
void print(int id, const std::string& msg) {
std::cout << "Thread " << id << ": " << msg << std::endl;
}
int main() {
std::thread t(print, 1, "Hello");
t.join();
return 0;
}
参数是按值传递的。即使函数声明的是引用,std::thread也会拷贝一份。为什么会这样?因为线程可能比调用者活得久,传引用的话,原对象可能已经销毁了。
小技巧:
如果你真的想传引用(比如修改外部变量),必须用std::ref包装:
void modify(int& x) { x = 42; }
int main() {
int val = 0;
std::thread t(modify, std::ref(val));
t.join();
// val 现在是 42
return 0;
}
传指针也要小心。我曾经犯过一个错误:把局部变量的指针传给线程,然后主线程继续执行,局部变量被销毁,线程还在用那个指针。嗯,结果就是段错误。所以,传指针时一定要保证对象的生命周期比线程长。
2.4 知识体系总览
下面这张图,把线程创建和管理的核心逻辑串起来了。你可以对照着看,心里有个谱:
2.5 避坑指南
最后,我总结几个实战中容易犯的错误:
| 错误场景 | 后果 | 正确做法 |
|---|---|---|
| 忘记join或detach | 程序崩溃(terminate) | RAII包装,析构时自动join |
| detach后访问局部变量 | 野指针、数据损坏 | 用堆对象或拷贝传值 |
| 多次join同一个线程 | 抛std::system_error | 先检查joinable() |
| 传引用却忘了std::ref | 修改的是副本,原变量不变 | 显式使用std::ref |
嗯,线程创建和管理,说白了就是三件事:怎么启动、怎么结束、怎么传参。把这三件事搞清楚了,后面学锁、学条件变量、学future,都会顺手很多。我个人建议,刚开始写多线程代码时,优先用join,少用detach。等你对线程生命周期有了感觉,再考虑detach的场景。
我的一个小习惯:
写一个ThreadGuard类,在析构时自动join。这样即使函数中途异常退出,线程也能被安全回收。代码量不大,但能省很多调试时间。