零、复习隐式转换和显式转换的概念

  • 隐式转换(系统根据程序的需要而自动转换的):
1
2
3
4
5
6
7
8
9
void fun(CTest test); 

class CTest
{
public:
CTest(int m = 0);
}

fun(20);//隐式转换,这里的20生成了一个临时对象
  • 显式转换(需要加括号标注想要转换的类型):
1
2
3
4
5
6
7
8
9
10
void fun(CTest test); 

//但有时隐式转换并不是我们想要的,可在构造函数前加上关键字explicit,来指定显式调用
class CTest
{
public:
explicit CTest(int m = 0);
}
fun(20);//错误,隐式转换
fun(static_cast<CTest>(20)); //正确,显式转换,这里的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
// thread::get_id / this_thread::get_id
#include <iostream> // std::cout
#include <thread> // std::thread, std::thread::id, std::this_thread::get_id
#include <chrono> // std::chrono::seconds

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() () {

}
};

// 为什么这里用的是const?本人也不太懂,大概是,因为线程参数生成了一个临时对象,这个临时对象是const的
//这里涉及的内容与左右值引用有关,以后会抽空去深究
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)); // 这里,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)); //注意,我这里使用了ref!
mythread.join();
return 0;
}

其中一次的输出结果为:

1
2
3
4
对象地址为:00AFFAF0 构造函数!线程id = 17904
main函数!线程id = 17904
print函数!对象地址为: 00AFFAF0 线程id = 10904
对象地址为:00AFFAF0 析构函数!线程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)
{
//如果线程从主线程detach了,
//i不是mvar真正的引用,实际上值传递,即使主线程运行完毕了,子线程用i仍然是安全的,但仍不推荐传递引用
//推荐改为const int i
cout << i << endl;
//pmybuf还是指向原来的字符串,所以这么写是不安全的
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();这样就不存在局部变量失效导致线程对内存的非法引用问题。