零、复习隐式转换和显式转换的概念
1 2 3 4 5 6 7 8 9 void fun (CTest test) ; class CTest { public : CTest (int m = 0 ); } fun (20 );
1 2 3 4 5 6 7 8 9 10 void fun (CTest test) ; class CTest { public : explicit CTest (int m = 0 ) ; } fun (20 );fun (static_cast <CTest>(20 ));
一、获得线程id
格式:std::thread::get_id
cpp官网解释:
Get thread id
Returns the thread id.
If the thread object is joinable, the function returns a > value that uniquely identifies the thread.
If the thread object is not joinable, the function returns a default-constructed object of member type thread::id.
Return value: An object of member type thread::id that uniquely identifies the thread (if joinable), or default-constructed (if not joinable).
这里说的意思是:如果线程对象的joinable()==true,那么将会返回一个独一无二的线程标识,此标识为一串整型数字。
官方给出了以下程序作为例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <iostream> #include <thread> #include <chrono> std::thread::id main_thread_id = std::this_thread::get_id (); void is_main_thread () { if ( main_thread_id == std::this_thread::get_id () ) std::cout << "This is the main thread.\n" ; else std::cout << "This is not the main thread.\n" ; } int main () { is_main_thread (); std::thread th (is_main_thread) ; th.join (); }
以上程序获取了两个线程id:一个是主线程的id(在主函数体外声明了一个std::thread::id类型的变量,并命名为main_thread_id),另一个是子线程的id。运行结果显而易见:
1 2 This is the main thread. This is not the main thread.
二、传递参数问题
前排提示:作为初学者,我们现在不必深究thread类的源码到底是怎么写的,而是可以通过测试的办法来得知thread类对象传参需要注意的问题,许多原理可以待以后再去深究。
1.值引用
首先需要记住一句话:std::thread的构造函数只会单纯的复制传入的变量,特别需要注意的是传递引用时,传入的是值的副本,也就是说子线程中的修改影响不了主线程中的值。 通俗的讲,使用&是没用的,thread类对象还是会固执的把参数复制构造一遍。
下面来看一个例子,请注意,在这个例子中,我定义了一个类,这个类的构造函数又称为类型转换构造函数;我定义了一个函数,这个函数的形参类型为该类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 #include <iostream> #include <thread> using namespace std;class A {public : int i; A (int _i):i (_i){ cout << "对象地址为:" << this << " 构造函数!线程id = " << this_thread::get_id () <<endl; } A (const A &a):i (a.i){ cout << "对象地址为:" << this << " 拷贝函数!线程id = " << this_thread::get_id ()<< endl; } ~A (){ cout << "对象地址为:" << this << " 析构函数!线程id = " << this_thread::get_id ()<< endl; } void operator () () { } }; void print (const A &a) { cout << "print函数!对象地址为: " << &a << " 线程id = " << this_thread::get_id () << endl; } int main () { int par = 10 ; cout << "main函数!线程id = " << this_thread::get_id () << endl; thread mythread (print, A(par)) ; mythread.join (); return 0 ; }
按照正常的思路,程序应该首先输出主线程id,然后临时对象构建、print函数、析构函数,最后程序结束了,整个过程应该只生成一个对象。但是实际情况并不是这个样子(这是其中一次测试结果):
1 2 3 4 5 6 main函数!线程id = 7616 对象地址为:008FFBB4 构造函数!线程id = 7616 对象地址为:00E7D5E0 拷贝函数!线程id = 7616 对象地址为:008FFBB4 析构函数!线程id = 7616 print函数!对象地址为: 00E7D5E0 线程id = 17176 对象地址为:00E7D5E0 析构函数!线程id = 17176
可以看出,输出中多了一个拷贝构造函数,生成了两个对象,这就是我们上面所说的:传递引用时,传入的是值的副本,也就是说子线程中的修改影响不了主线程中的值。 并且临时对象都是在主函数中已经构建好,再传入子线程,即使detach也不会出错。
如果我们把void print(const A &a)改成void print(const A a),那么结果就会更加繁琐(这是其中一次测试结果,生成了三个对象):
1 2 3 4 5 6 7 8 main函数!线程id = 19024 对象地址为:0099F74C 构造函数!线程id = 19024 对象地址为:00D9ED60 拷贝函数!线程id = 19024 对象地址为:0099F74C 析构函数!线程id = 19024 对象地址为:0148F744 拷贝函数!线程id = 8572 print函数!对象地址为: 0148F744 线程id = 8572 对象地址为:0148F744 析构函数!线程id = 8572 对象地址为:00D9ED60 析构函数!线程id = 8572
2.传引用
由以上内容我们知道,即使是用引用来接收传的值,也是会将其拷贝一份到子线程的独立内存中 。这种现象可真不是什么好事:一方面,因为构建了不必要的临时对象,使得程序运行时间和空间都有所浪费;另一方面,这样也不利于我们通过子线程来修改对象的值。于是,C++中引入了std::ref()。
下面通过一个程序(其实就是把上面程序修改了一下)来说明std::ref()的作用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 #include <iostream> #include <thread> using namespace std;class A {public : int i; A (int _i):i (_i){ cout << "对象地址为:" << this << " 构造函数!线程id = " << this_thread::get_id () <<endl; } A (const A &a):i (a.i){ cout << "对象地址为:" << this << " 拷贝函数!线程id = " << this_thread::get_id ()<< endl; } ~A (){ cout << "对象地址为:" << this << " 析构函数!线程id = " << this_thread::get_id ()<< endl; } void operator () () { } }; void print (const A &a) { cout << "print函数!对象地址为: " << &a << " 线程id = " << this_thread::get_id () << endl; } int main () { int par = 10 ; A a (par) ; cout << "main函数!线程id = " << this_thread::get_id () << endl; thread mythread (print, ref(a)) ; mythread.join (); return 0 ; }
其中一次的输出结果为:
1 2 3 4 对象地址为:00 AFFAF0 构造函数!线程id = 17904 main函数!线程id = 17904 print函数!对象地址为: 00 AFFAF0 线程id = 10904 对象地址为:00 AFFAF0 析构函数!线程id = 17904
观察对象地址,这时子线程也指向了主线程的对象,再也没有调用拷贝构造函数了。这里引出了一个很重要的结论:传入类对象时,使用引用来接收比用值接收更高效。
注意事项:
其实上面这个程序中void print(const A &a)已经没有必要加const了。因为如果我们使用传引用,那么对象里的值一般会被我们改变的。
ref(a)不能改为&a,VS2019编译会报错。
如果把ref(a)改为par,输出结果也不会有拷贝构造函数,这里par被隐式类型转换成A对象。具体原因未知。
由于子线程的对象指向了主线程的对象,如果detach了,这样是不安全的,因为存在这种情况:主线程运行完毕,par被回收,导致程序出错。推荐依然写成A(par)。
作为初学者的我,不是很明白以上这些现象背后的原理,只好先记住,以后有机会再去深究源码了。
3.传指针时需要注意的点
这里是一个在B站学习多线程(C++11并发多线程视频教程 )时看到的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include <iostream> #include <thread> using namespace std;void myPrint (const int &i, char * pmybuf) { cout << i << endl; cout << pmybuf << endl; } int main () { int mvar = 1 ; int & mvary = mvar; char mybuf[] = "this is a test" ; thread myThread (myPrint, mvar, mybuf) ; myThread.join (); return 0 ; }
总结:使用detach就不要用引用,不要传指针,也不要隐式转换。
终极结论:建议不使用detach(),只使用join();这样就不存在局部变量失效导致线程对内存的非法引用问题。