线程可以共享进程的内存空间,线程拥有自己独立内存。
关于参数的传递,
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下要避免隐式转换,因为此时子线程可能还来不及转换主线程就结束了,应该在构造线程时,用参数构造一个临时对象传入。