1. 简介
thread库是C++11新增的<thread>头文件提供的跨平台线程管理工具,它的优势在于,我们不需要再关注平台差异,thread库屏蔽了底层的实现差异,可以在不同的系统下运行。
2. 基础准备
-
必须包含头文件:#include<thread>。
-
可调用对象:std::thread 构造函数支持传入以下可执行逻辑
lambda表达式(最常用)
普通函数/函数指针
类成员函数
重载operator()的仿函数对象
线程对象构造完成的瞬间,线程就会立即启动(不是等join()才启动)。
下面我们根据上面列出的依次进行举例说明。
3. 核心用法
3.1 基于 lambda 表达式创建线程(最常用)
cpp
#include <iostream>
#include <thread>
using namespace std;
int main() {
// lambda直接作为线程执行体,无需定义额外函数
thread t([]{
for (int i = 0; i < 3; i++) {
cout << "子线程执行中,i=" << i << endl;
}
});
t.join(); // 主线程等子线程结束
cout << "主线程继续执行" << endl;
return 0;
}
运行结果:
子线程执行中,i=0
子线程执行中,i=1
子线程执行中,i=2
子线程执行中,i=3
子线程执行中,i=4
子线程执行中,i=5
主线程继续执行
3.2 基于普通函数创建线程
cpp
#include <iostream>
#include <thread> // 必须包含的头文件
using namespace std;
// 线程要执行的普通函数
void print_num(int num, const string& msg) {
cout << "子线程执行:" << msg << ",数字:" << num << endl;
}
int main() {
// 构造thread对象:参数=函数名 + 函数的实参
// 构造完成的瞬间,子线程立即启动
thread t(print_num, 10, "hello thread");
// 主线程等待子线程执行完毕(必须!否则子线程可能被强制终止)
t.join();
return 0;
}
运行结果:
子线程执行:hello thread,数字:10
3.3 基于类成员函数创建线程
cpp
#include <iostream>
#include <thread>
#include <string>
using namespace std;
class MyClass {
public:
// 类的成员函数(线程要执行的逻辑)
void show_msg(const string& msg) {
cout << "成员函数线程:" << msg << endl;
}
};
int main() {
MyClass obj; // 创建类对象
// 构造参数:成员函数地址 + 对象指针 + 函数实参
thread t(&MyClass::show_msg, &obj, "类成员函数测试");
t.join();
return 0;
}
运行结果:
成员函数线程:类成员函数测试
3.4 基于重载operator()的仿函数对象创建线程
cpp
#include <iostream>
#include <thread>
using namespace std;
// 定义"函数对象类":重载operator()
class MyFuncObj {
public:
// 构造函数:初始化成员变量(函数对象可以携带状态,这是普通函数没有的优势)
MyFuncObj(int init_val) : count(init_val) {}
// 重载operator():线程要执行的逻辑写在这里
void operator()(const string& msg) {
for (int i = 0; i < 3; i++) {
// 可以访问类的成员变量(函数对象的"状态")
cout << "函数对象执行:" << msg << ",当前计数:" << ++count << endl;
}
}
private:
int count; // 函数对象的成员变量(可以携带状态)
};
int main() {
// 1. 创建仿函数对象(同时初始化成员变量count=0)
MyFuncObj func_obj(0);
// 2. 将仿函数对象传给std::thread的构造函数
// 线程会执行func_obj的operator(),并传入实参"test"
thread t(func_obj, "hello");
// 3. 等待线程执行完毕
t.join();
return 0;
}
这里我们实现了一个类,它重载了operator(),其实这里本质就是仿函数,我们利用类实例化出一个仿函数对象,再讲这个仿函数对象传给thread,这时线程就会调用operator()函数执行线程的相关任务。(线程会自动调用func_obj.operator(实参)(即示例中的func_obj("hello"))。)
3.5 等待线程结束(join)
thread库中为我们提供了join()函数,它的功能就是等待对应线程结束,线程创建好后必须有等待,如果不调用join函数,那么主线程就会提前结束,主线程一旦终止,子线程也会立即终止,这就可能导致子线程的逻辑还没执行完毕就被强制退出了,这是不合理的。
cpp
#include <iostream>
#include <thread>
#include <chrono> // 用于休眠
using namespace std;
int main() {
thread t([]{
// 子线程休眠2秒(模拟耗时操作)
this_thread::sleep_for(chrono::seconds(2));
cout << "子线程执行完毕" << endl;
});
// 注释掉join,主线程直接结束,子线程被强制终止
// t.join();
cout << "主线程提前结束" << endl;
return 0;
}
3.6 分离线程(detach)
detach() 作用是:将子线程与主线程 "解绑",子线程进入 "后台运行" 状态:
- 主线程无需等待子线程结束,可继续执行;
- 子线程的资源由操作系统接管,执行完后自动释放;
- 解绑后,主线程无法再通过
join()等待该子线程。
cpp
#include <iostream>
#include <thread>
#include <chrono>
using namespace std;
int main() {
thread t([]{
this_thread::sleep_for(chrono::seconds(2));
cout << "detach子线程执行完毕" << endl;
});
t.detach(); // 解绑线程,后台运行
cout << "主线程无需等待,直接执行" << endl;
// 主线程休眠3秒(否则主线程太快结束,进程销毁,detach线程也会终止)
this_thread::sleep_for(chrono::seconds(3));
return 0;
}
运行结果:
主线程无需等待,直接执行
detach子线程执行完毕
3.7 判断线程状态(joinable)
joinable() 返回bool值,用于判断线程是否 "可被join()/detach()":
- 返回
true:线程未调用过join()/detach(),且未执行完毕; - 返回
false:线程已调用join()/detach(),或线程对象是空的。
功能:避免重复调用join()/detach()(会直接导致程序崩溃)。
cpp
#include <iostream>
#include <thread>
using namespace std;
void func() {
cout << "子线程执行" << endl;
}
int main() {
thread t(func);
// 先判断,再操作(安全写法)
if (t.joinable()) {
t.join();
cout << "第一次join成功" << endl;
}
// 再次判断,此时返回false(已join)
if (!t.joinable()) {
cout << "线程已不可join" << endl;
}
// 错误:重复join会崩溃,所以必须用joinable判断
// t.join();
return 0;
}
运行结果:
子线程执行
第一次join成功
线程已不可join
3.8 线程的移动(不支持拷贝)
std::thread 对象不能拷贝(一个线程资源不能被多个thread对象管理),但可以移动(转移线程的所有权)。
3.8.1 直接移动线程对象
cpp
#include <iostream>
#include <thread>
using namespace std;
void func() {
cout << "被移动的线程执行" << endl;
}
int main() {
thread t1(func); // 创建线程t1
// 错误:不能拷贝(编译器直接报错)
// thread t2 = t1;
// 正确:移动,t1的所有权转移给t2,t1变为"不可joinable"
thread t2 = move(t1);
t2.join(); // 只能操作t2,t1已无效
return 0;
}
运行结果:
被移动的线程执行
这里大家需要注意一下,thread是不允许拷贝的,所以这里我们使用move进行转移,使用move后,它就变成了右值,于是这里就需要调用移动构造来构造t2对象。
3.8.2 容器存储线程
cpp
#include <iostream>
#include <thread>
#include <vector>
using namespace std;
void func(int num) {
cout << "线程" << num << "执行" << endl;
}
int main() {
vector<thread> pool; // 存储线程的容器
// 方式1:emplace_back(推荐,原地构造线程,无临时对象)
for (int i = 0; i < 2; i++) {
pool.emplace_back(func, i);
}
// 方式2:push_back + move(先构造临时线程,再移动)
thread t(func, 2);
pool.push_back(move(t));
// 等待所有线程结束
for (auto& th : pool) {
if (th.joinable()) {
th.join();
}
}
return 0;
}
3.9 获取线程id
cpp
#include <iostream>
#include <thread>
using namespace std;
int main() {
// 获取主线程ID
cout << "主线程ID:" << this_thread::get_id() << endl;
thread t([]{
// 获取子线程ID
cout << "子线程ID:" << this_thread::get_id() << endl;
});
// 获取线程对象的ID
cout << "线程t的ID:" << t.get_id() << endl;
t.join();
return 0;
}
运行结果:
主线程ID:11860
线程t的ID:14500
子线程ID:14500
4. 避坑指南
4.1 线程对象析构前未调用 join ()/detach ()(最致命)
线程对象销毁时(比如出作用域),若既没join()也没detach(),程序会调用std::terminate()直接崩溃。
这里我们可以通过join函数,也可以通过detach函数,建议使用join函数进行等待。
cpp
#include <iostream>
#include <thread>
using namespace std;
void test() {
thread t([]{ cout << "子线程执行" << endl; });
// 未join/detach,t析构时程序崩溃
}
int main() {
test();
return 0;
}
4.2 detach () 线程访问主线程局部变量
cpp
#include <iostream>
#include <thread>
#include <string>
#include <chrono>
using namespace std;
void test() {
string msg = "主线程局部变量"; // 局部变量,test()结束后销毁
thread t([&]{
// detach后,test()先结束,msg已销毁,访问会崩溃
this_thread::sleep_for(chrono::seconds(1));
cout << msg << endl;
});
t.detach();
}
int main() {
test();
this_thread::sleep_for(chrono::seconds(2));
return 0;
}
detach()后的线程后台运行,若主线程先结束,其局部变量会被销毁,子线程再访问会导致 "野指针 / 非法内存访问",程序崩溃。
解决方案:让detach()线程访问全局变量,或拷贝变量到线程内部(不用引用)
cpp
void test() {
string msg = "主线程局部变量";
thread t([msg]{ // 拷贝msg到线程内部,不再依赖主线程
this_thread::sleep_for(chrono::seconds(1));
cout << msg << endl;
});
t.detach();
}
这里补充一点:
线程并不是越多,效率就越高,线程数超过 CPU 核心数时,操作系统会频繁切换线程上下文(保存 / 恢复线程状态),反而增加开销,导致程序变慢。
所以我们在创建线程的时候一定要适量创建,不要创建太多线程。
5. this_thread
std::this_thread是 C++11 <thread>头文件中提供的命名空间(不是类);
核心作用是:操作 "当前正在执行代码的那个线程"(即调用这些函数的线程本身)。
下面介绍一下常用函数:
5.1 get_id
返回当前线程 的唯一标识(类型是std::thread::id),用于区分不同线程(比如调试时打印线程 ID)。
cpp
#include <iostream>
#include <thread>
using namespace std;
void func() {
// 获取当前线程(子线程)的ID
cout << "子线程ID:" << this_thread::get_id() << endl;
}
int main() {
// 获取当前线程(主线程)的ID
cout << "主线程ID:" << this_thread::get_id() << endl;
thread t(func);
t.join();
return 0;
}
5.2 sleep_for
让当前线程暂停执行指定的时间长度(比如休眠 2 秒),常用于模拟耗时操作、控制线程执行节奏。
cpp
template <class Rep, class Period>
void sleep_for(const chrono::duration<Rep, Period>& rel_time);
- 参数是
std::chrono库的 "时间段"(比如chrono::seconds(2)表示 2 秒,chrono::milliseconds(500)表示 500 毫秒); - 需要包含头文件
<chrono>。
cpp
#include <iostream>
#include <thread>
#include <chrono> // 必须包含,用于表示时间
using namespace std;
int main() {
cout << "主线程开始休眠..." << endl;
// 让当前线程(主线程)休眠2秒
this_thread::sleep_for(chrono::seconds(2));
cout << "主线程休眠结束!" << endl;
return 0;
}