线程基础:std::thread的创建与启动、线程的join与detach、传递参数给线程函数
好,咱们今天来聊聊C++并发编程里最基础、也最核心的东西——std::thread。说实话,我刚开始接触多线程那会儿,觉得这东西挺玄乎的。后来在项目里踩过几次坑,才慢慢摸清楚它的脾气。今天我就把这些年积累的经验,掰开了揉碎了讲给你听。
一、std::thread的创建与启动
创建一个线程,说白了就是告诉系统:“嘿,帮我开一条新的执行流,跑这个函数。”C++11开始,标准库给了我们std::thread这个利器。用法很简单,你只要把要执行的函数(或者可调用对象)传给线程构造函数就行。
#include <iostream>
#include <thread>
void hello() {
std::cout << "Hello from thread!" << std::endl;
}
int main() {
std::thread t(hello); // 创建并启动线程
t.join(); // 等待线程结束
return 0;
}
你看,就这么几行代码,一个线程就跑起来了。线程一旦被创建,它会立刻开始执行你给它的函数。这里有个细节要注意:线程对象必须与线程的生命周期关联起来,否则程序会崩溃。嗯,这里说的关联,就是后面要讲的join或detach。
二、线程的join与detach
线程创建后,你必须做二选一:要么join,要么detach。没有第三条路。为什么?因为线程对象析构时,如果它仍然“可结合”(joinable),程序会调用std::terminate直接终止。说白了,就是C++强制你负责任地管理线程。
2.1 join:等待线程结束
join()的意思是:当前线程(比如主线程)停下来,等这个子线程跑完,再继续往下走。这是一种同步操作。
std::thread t(worker);
t.join(); // 主线程阻塞在这里,直到worker执行完毕
我在项目中遇到过这样的情况:主线程需要子线程的计算结果才能继续处理。这时候用join就非常合适。但要注意,join会让当前线程阻塞,如果子线程执行时间很长,主线程就卡住了。你想想看,如果UI线程里调了join,界面就会无响应,用户体验极差。
2.2 detach:让线程在后台运行
detach()则是另一种态度:“你走吧,我不等你了。”线程与线程对象分离,线程在后台独立运行,C++运行时库会负责回收它的资源。
std::thread t(background_worker);
t.detach(); // 线程在后台自由运行
// 注意:此时不能再调用t.join(),会抛异常
detach了一个线程,这个线程里访问了主线程的局部变量。结果主线程函数返回了,局部变量被销毁,子线程还在访问那块内存……嗯,典型的悬空引用。所以detach的线程里,千万别碰主线程的局部变量,要么传值,要么用堆上的对象。
2.3 joinable判断
你可以用joinable()检查线程是否可以被join或detach。一个线程只能被join或detach一次,重复调用会抛异常。
if (t.joinable()) {
t.join();
}
这个检查我建议养成习惯。尤其是在异常处理中,确保线程资源被正确释放。
三、传递参数给线程函数
线程函数往往需要参数。怎么传?直接跟在构造函数的第二个参数后面就行。但这里头有门道,我慢慢说。
3.1 基本传参方式
void print_sum(int a, int b) {
std::cout << a + b << std::endl;
}
std::thread t(print_sum, 3, 4);
t.join();
参数会以值传递的方式拷贝到线程内部。这很重要,因为线程和主线程是并发的,你不能保证主线程的变量在线程执行期间还活着。
3.2 传递引用参数
如果你想让线程修改外部变量,或者传递大对象避免拷贝,就需要用引用。但直接传引用会编译失败,因为std::thread会忽略引用,仍然拷贝参数。解决办法是用std::ref包装。
void modify(int& x) {
x = 42;
}
int value = 0;
std::thread t(modify, std::ref(value));
t.join();
// value 现在是 42
std::thread默认是值传递。用std::ref显式告诉编译器:“我要传引用!”另外,要确保被引用的对象在线程执行期间一直有效,否则就是悬空引用。
3.3 传递成员函数指针
如果你想让线程执行某个类的成员函数,需要传递对象指针(或引用)和成员函数指针。
class Worker {
public:
void run(int task_id) {
std::cout << "Task " << task_id << std::endl;
}
};
Worker w;
std::thread t(&Worker::run, &w, 1);
t.join();
这里&Worker::run是成员函数指针,&w是对象地址。线程内部会调用w.run(1)。
3.4 传递unique_ptr等移动语义对象
有些对象只能移动,不能拷贝,比如std::unique_ptr。这时候需要用std::move显式转移所有权。
void take_ownership(std::unique_ptr<int> p) {
std::cout << *p << std::endl;
}
auto ptr = std::make_unique<int>(100);
std::thread t(take_ownership, std::move(ptr));
t.join();
// 此时ptr已经为空
你想想看,如果不用std::move,std::thread会尝试拷贝unique_ptr,但拷贝构造函数是被删除的,编译就会报错。所以移动语义在这里是必须的。
四、知识体系总览
下面这张图,我把本章的核心知识点串起来了。你可以对照着看,心里有个谱。
五、总结与建议
好了,关于std::thread的基础知识,咱们就聊到这儿。我最后给你几个实用建议:
- 优先用join:除非你非常清楚子线程的生命周期,否则
join比detach安全得多。我个人的项目里,90%的情况都用join。 - 传参时注意生命周期:无论是传值、传引用还是传指针,都要确保参数在线程执行期间有效。这是并发编程里最常见的bug来源。
- RAII管理线程:可以写一个包装类,在析构函数里自动
join或detach,避免忘记。C++20的std::jthread就是干这个的,不过那是后话了。
线程基础就这些内容。你把这些搞明白了,后面学锁、条件变量、异步任务什么的,就会轻松很多。记住一句话:并发编程,细节是魔鬼。每个API背后都有它的设计哲学和陷阱,多写多练,慢慢就熟了。