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表达式
  • 线程对象必须被joindetach,否则析构时会调用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 知识体系总览

下面这张图,把线程创建和管理的核心逻辑串起来了。你可以对照着看,心里有个谱:

线程创建与管理核心流程 std::thread 构造 线程立即开始执行 join() detach() 主线程阻塞等待 线程结束后继续 线程独立运行 主线程不等待 参数传递:默认按值拷贝 传引用:用 std::ref 传指针:注意生命周期 传对象:确保可拷贝 传lambda:最灵活 必须 join 或 detach,否则 terminate()

2.5 避坑指南

最后,我总结几个实战中容易犯的错误:

错误场景 后果 正确做法
忘记join或detach 程序崩溃(terminate) RAII包装,析构时自动join
detach后访问局部变量 野指针、数据损坏 用堆对象或拷贝传值
多次join同一个线程 抛std::system_error 先检查joinable()
传引用却忘了std::ref 修改的是副本,原变量不变 显式使用std::ref

嗯,线程创建和管理,说白了就是三件事:怎么启动、怎么结束、怎么传参。把这三件事搞清楚了,后面学锁、学条件变量、学future,都会顺手很多。我个人建议,刚开始写多线程代码时,优先用join,少用detach。等你对线程生命周期有了感觉,再考虑detach的场景。

我的一个小习惯:

写一个ThreadGuard类,在析构时自动join。这样即使函数中途异常退出,线程也能被安全回收。代码量不大,但能省很多调试时间。


公众号:蓝海资料掘金营,微信deep3321