std::thread详解
1. std::thread基本介绍
1)构造std::thread对象时,如果不带参则会创建一个空的thread对象,但底层线程并没有真正被创建,一般可将其他std::thread对象通过move移入其中;
如果带参则会创建新线程,而且会被立即运行。
2)joinable():用于判断std::thread对象联结状态,一个std::thread对象只可能处于可联结或不可联结两种状态之一。
a. 可联结:当线程己运行或可运行、或处于阻塞时是可联结的。注意,如果某个底层线程已经执行完任务,但是没有被join的话,仍然处于joinable状态。
即std::thread对象(对象由父线程所有)与底层线程保持着关联时,为joinable状态。
b. 不可联结:
① 当不带参构造的std::thread对象为不可联结,因为底层线程还没创建。
② 己移动的std::thread对象为不可联结。
③ 己调用join或detach的对象为不可联结状态。因为调用join()以后,底层线程己结束,而detach()会把std::thread对象和对应的底层线程之间的连接断开。
join():等待子线程,调用线程处于阻塞模式。join()执行完成之后,底层线程id被设置为0,即joinable()变为false。
detach():分离子线程,与当前线程的连接被断开,子线程成为后台线程,被C++运行时库接管。
3)std::thread对象析构时,会先判断是否可joinable(),如果可联结,则程序会直接被终止出错。这意味着创建thread对象以后,要在随后的某个地方调用join或
detach以便让std::thread处于不可联结状态。
4)std::thread对象不能被复制和赋值,只能被移动。
5)获取当前信息
// t为std::thread对象 t.get_id(); // 获取线程ID t.native_handle(); // 返回与操作系统相关的线程句柄 std::thread::hardware_concurrency(); // 获取CPU核数,失败时返回0
6)std::this_thread命名空间中相关辅助函数
get_id(); // 获取线程ID yield(); // 当前线程放弃执行,操作系统转去调度另一线程 sleep_until(const xtime* _Abs_time); // 线程休眠至某个指定的时刻(time point),该线程才被重新唤醒 sleep_for(std::chrono::seconds(3)); // 睡眠3秒后才被重新唤醒,不过由于线程调度等原因,实际休眠时间可能比 sleep_duration 所表示的时间片更长
3. 传递参数的方式(2次传参)
a. 第一次传参(向 std::thread 构造函数传参):在创建thread对象时,std::thread构建函数中的所有参数均会按值并以副本的形式保存成一个tuple对象。
该tuple由调用线程(一般是主线程)在堆上创建,并交由子线程管理,在子线程结束时同时被释放。
注:如果要达到按引用传参的效果,可使用std::ref来传递。
b. 第二次传参(向线程函数的传参):由于std::thread对象里保存的是参数的副本,为了效率同时兼顾一些只移动类型的对象,所有的副本均被
std::move到线程函数,即以右值的形式传入,所以最终传给线程函数参数的均为右值。
现在我们根据线程参数的类型展开讨论:
首先先给出一个用作测试的类
class Test { public: mutable int mutableInt = 0; Test() : mutableInt(0) { cout << this << " " << "Test()" << endl; } Test(int i) : mutableInt(i) { cout << this << " " << "Test(int i)" << endl; } Test(const Test& w) : mutableInt(w.mutableInt) { cout << this << " " << "Test(const Test& w)" << endl; } Test(Test&& w) noexcept // 移动构造 { mutableInt = w.mutableInt; cout << this << " " << "Test(Test && w)" << endl; } void func(const string& s) { cout <<"void func(string& s)" << endl; } };
a. 线程参数为const T&类型:这里的引用是针对于第二次传递的参数,也就是直接引用保存在tuple中的参数副本,而不是最原始的参数,即
不是main中变量。但是你如果第一次向std::thread传参时使用std::ref,首先会创建一个std::ref(它也是一个类)临时对象,里面会保存着最原始
变量(main中的变量)的引用,然后这个std::ref临时对象再以副本的形式保存在std::thread中,随后这个副本被move到线程函数。由于std::ref
重载了类型转换运算符operator T&(),因此会隐式转换为Test&类型,因此起到的效果就好象main中的变量直接被按引用传递到线程函数中来。
现在按照这个理解来分析下面的代码:
对于std::thread t1(test_ctor, w); 首先会调用一次拷贝构造,把w存储为tuple元素;然后因为线程参数是引用,所以tuple元素给线程传参数时不会发生
拷贝,但实际运行结果发现多输出了一次拷贝,这应该是std::thread隐藏的实现细节,需要阅读源码了。
如果使用std::ref包装的话,内部引用了原始的w,所以不会发生拷贝,但会发生std::ref对象的拷贝。
void test_ctor(const Test& w) { cout << &w << " " << "w.matableInt = " << ++w.mutableInt << endl; } int main() { Test w; // std::thread默认的按值传参方式: 所有的实参都是被拷贝到std::thread对象的tuple中,即以副本形式被保存起来。 // 注意,w是按值保存到std::thread中的,会调用其拷贝构造函数。外部的w没受影响。mutableInf仍为0。 std::thread t1(test_ctor, w); t1.join(); cout << "w.mutableInt = " << w.mutableInt << endl << endl; // std::thread按引用传参(std::ref), 因为w是按引用传入到std::ref对象中的,不会调用其拷贝构造函数。 // 由于w按引用传递,mutableInf被修改为1。 std::thread t2(test_ctor, std::ref(w)); t2.join(); cout << "w.mutableInt = " << w.mutableInt << endl; return 0; } // 第一部分输出如下 Test(const Test& w) // 调用拷贝构造函数生成std::thread中的副本对象 Test(Test && w) // std::thread中的副本移动到线程参数 w.mutableInt = 0 // 第二部分 w.mutableInt = 1
b. 线程参数为T&类型:最终传给线程函数参数的均为右值,而T&类型是不接受右值的,使用std::ref包装后便不报错了,因为它能隐式转换成 T&。
void updateTest_ref(Test& w) { cout << &w << " " << "invoke updateTest_ref" << endl; } int main() { Test w; std::thread t1(updateTest_ref, w); // 编译失败,因为std::thread内部是以右值形式向线程函数updateTest_ref(Test&)传参的, // 而右值无法用来初始化Test&引用。 std::thread t3(updateTest_ref, std::ref(w)); // ok, 原因类似test_ctor函数中的分析。即当线程函数的形参为T&时,一般以std::ref形式传入 t3.join(); }