线程可以共享进程的内存空间,线程拥有自己独立内存。
关于参数的传递,std::thread的构造函数只会单纯的复制传入的变量,特别需要注意的是传递引用时,传入的是值的副本,也就是说子线程中的修改影响不了主线程中的值。
值传递
主线程中的值,被拷贝一份传到了子线程中。
1 #include <iostream> 2 #include <thread> 3 4 using namespace std; 5 6 void test(int ti, int tj) 7 { 8 cout << "子线程开始" << endl; 9 //ti的内存地址0x0055f69c {4},tj的内存地址0x0055f6a0 {5} 10 cout << ti << " " << tj << endl; 11 cout << "子线程结束" << endl; 12 return; 13 } 14 15 16 int main() 17 { 18 cout << "主线程开始" << endl; 19 //i的内存地址0x001efdfc {4},j的内存地址0x001efdf0 {5} 20 int i = 4, j = 5; 21 thread t(test, i, j); 22 t.join(); 23 cout << "主线程结束!" << endl; 24 return 0; 25 }
传引用
从下面的运行结果,可以看出,即使是用引用来接收传的值,也是会将其拷贝一份到子线程的独立内存中,这一点与我们编写普通程序时不同。这是因为线程的创建属于函数式编程,所以为了传引用C++中才引入了std::ref()。关于std::ref()。
1 #include <iostream> 2 #include <thread> 3 4 using namespace std; 5 6 class A{ 7 public: 8 int ai; 9 A (int i): ai(i) { } 10 }; 11 12 //这种情况必须在引用前加const,否则会出错。目前本人的觉得可能是因为临时对象具有常性 13 void test(const int &ti, const A &t) 14 { 15 cout << "子线程开始" << endl; 16 //ti的内存地址0x0126d2ec {4},t.ai的内存地址0x0126d2e8 {ai=5 } 17 cout << ti << " " << t.ai << endl; 18 cout << "子线程结束" << endl; 19 return; 20 } 21 22 23 int main() 24 { 25 cout << "主线程开始" << endl; 26 //i的内存地址0x010ff834 {4},a的内存地址0x010ff828 {ai=5 } 27 int i = 4; 28 A a = A(5); 29 thread t(test, i, a); 30 t.join(); 31 cout << "主线程结束!" << endl; 32 return 0; 33 }
那么如果我们真的需要像一般程序那样传递引用呢,即在子线程中的修改能够反映到主线程中。此时需要使用std::ref()。但是注意如果我们会在子线中改变它,此时用于接收ref()的那个参数前不能加const。关于C++多线程中的参数传引用问题,我目前只是记住了这个现象,关于原理还需后期研究源码继续学习。
1 #include <iostream> 2 #include <thread> 3 4 using namespace std; 5 6 class A { 7 public: 8 int ai; 9 A(int i) : ai(i) { } 10 }; 11 12 //接收ref()的那个参数前不能加const,因为我们会改变那个值 13 void test(int& ti, const A& t) 14 { 15 cout << "子线程开始" << endl; 16 cout << ti << " " << t.ai << endl; 17 ti++; 18 cout << "子线程结束" << endl; 19 return; 20 } 21 22 23 int main() 24 { 25 cout << "主线程开始" << endl; 26 int i = 4; 27 A a = A(5); 28 thread t(test, ref(i), a); 29 t.join(); 30 cout << "i改变:" << i << endl; 31 cout << "主线程结束!" << endl; 32 return 0; 33 }
传入类对象时,使用引用来接收比用值接收更高效。
1 #include <iostream> 2 #include <thread> 3 4 using namespace std; 5 6 class A { 7 public: 8 int ai; 9 A (int i) : ai(i) 10 { 11 cout << "构造" << this << endl; 12 } 13 14 A (const A& a) :ai(a.ai) { 15 cout << "拷贝构造" << this << endl; 16 } 17 18 ~A() 19 { 20 cout << "析构" << this << endl; 21 } 22 }; 23 24 //void test(const A a) 25 void test(const A& a) 26 { 27 cout << "子线程开始" << endl; 28 cout << "子线程结束" << endl; 29 return; 30 } 31 32 33 int main() 34 { 35 cout << "主线程开始" << endl; 36 int i = 4; 37 thread t(test, A(i)); 38 t.join(); 39 cout << "主线程结束!" << endl; 40 return 0; 41 }
传指针
从下面的运行结果,可以看出,主线程和子线程中的指针都是指向同一块内存。所以在这种情况下会有一个陷阱,如果使用detach(),则当主线程崩溃或者正常结束后,该块内存被回收,若此时子线程没有结束,那么子线程中指针的访问将未定义,程序会出错。
1 #include <iostream> 2 #include <thread> 3 4 using namespace std; 5 6 7 void test(char *p) 8 { 9 cout << "子线程开始" << endl; 10 //0x004ffeb4 "hello" 11 cout << p << endl; 12 cout << "子线程结束" << endl; 13 return; 14 } 15 16 17 int main() 18 { 19 cout << "主线程开始" << endl; 20 //0x004ffeb4 "hello" 21 char s[] = "hello"; 22 thread t(test, s); 23 t.join(); 24 cout << "主线程结束!" << endl; 25 return 0; 26 }
传临时对象
用临时变量作为实参时,会更高效,由于临时变量会隐式自动进行移动操作,这就减少了整体构造函数的调用次数。而一个命名变量的移动操作就需要std::move()。
1 #include <iostream> 2 #include <thread> 3 4 using namespace std; 5 6 class A { 7 public: 8 int ai; 9 A (int i) : ai(i) 10 { 11 cout << "构造" << this << endl; 12 } 13 14 A (const A& a) :ai(a.ai) { 15 cout << "拷贝构造" << this << endl; 16 } 17 18 ~A() 19 { 20 cout << "析构" << this << endl; 21 } 22 }; 23 24 void test(const A& a) 25 { 26 cout << "子线程开始" << endl; 27 cout << "子线程结束" << endl; 28 return; 29 } 30 31 32 int main() 33 { 34 cout << "主线程开始" << endl; 35 int i = 4; 36 thread t(test, A(i)); 37 t.join(); 38 cout << "主线程结束!" << endl; 39 return 0; 40 }
总结
1、使用引用和指针是要注意;
2、对于内置简单类型,建议传值;
3、对于类对象,建议使用引用来接收,以为使用引用会只会构造两次,而传值会构造三次;
4、在detach下要避免隐式转换,因为此时子线程可能还来不及转换主线程就结束了,应该在构造线程时,用参数构造一个临时对象传入。