C++ 并发与多线程学习笔记(三)线程传参隐患 成员函数指针做线程函数

传递临时对象作为线程参数

先来看一个例子:

#include 
#include 
using namespace std;
void myprint(const int &i,char *pmybuf)
{
	cout << i << endl;
	cout << pmybuf << endl;
	return;
}

int main()
{
	int mvar = 1;
	int &mvary = mvar;
	char mybuf[] = "this is a test!";
	thread mytobj(myprint, mvar, mybuf);
	mytobj.detach();
	cout << "主线程结束" << endl;
	system("pause");
	return 0;
}

在函数中,传入了一个引用值i,通过调试来分析i:
(我用的是x64)
C++ 并发与多线程学习笔记(三)线程传参隐患 成员函数指针做线程函数_第1张图片
C++ 并发与多线程学习笔记(三)线程传参隐患 成员函数指针做线程函数_第2张图片
C++ 并发与多线程学习笔记(三)线程传参隐患 成员函数指针做线程函数_第3张图片
可以看到,在运行过程中,主线程中的值mvar和它的引用 mvary都是指向的同一个位置,但我们传入线程函数中的引用i的地址却不是mvar的地址,这个现象可以说明,传入线程中的并不是值的引用,实际是值传递。那么即使我们detach了子线程,子线程中的i仍然是安全的。
我们再来观察第二个char*的参数
C++ 并发与多线程学习笔记(三)线程传参隐患 成员函数指针做线程函数_第4张图片
C++ 并发与多线程学习笔记(三)线程传参隐患 成员函数指针做线程函数_第5张图片

和引用不同,传入的指针并没有发生改变,因此传入指针,对于线程来说是不安全的。那我们要让线程处理字符串的时候该怎么办呢?通常情况下想到的是用其他字符串类型来处理,比如std::string,它有c_str()可以方便的转换为char*的字符串,所以我们修改函数为:

void myprint(const int &i,const std::string &pmybuf)
{
	cout << i << endl;
	cout << pmybuf << endl;
	return;
}

运行以后发现地址确实已经和主线程不一样了。但这里有一个新的问题,mybuf是在什么时候被实例化std::string的?
通过上次的学习,我们知道主线程可能比子线程要先结束,那么就可能存在,mybuf都已经被回收了(main函数结束),系统才去实例化string的可能。
通过查阅资料和测试,我们在传入参数的时候我们就需要将mybuf实例化为string,通过临时对象,就能让其在线程中稳定。

thread mytobj(myprint,mvar,std::string(mybuf));

为了验证这一现象,我们用一个类来测试。测试代码如下:

#include 
#include 
using namespace std;
class A
{
public:
	int m_i;
	A(int a) :m_i(a)
	{
		cout << "A:A(int a)构造函数执行" << endl;
	}
	A(const A &a) :m_i(a.m_i)
	{
		cout << "A:A(const A &a)构造函数执行" << endl;
	}
	~A()
	{
		cout << "析构函数执行" << endl;
	}
};

void myprint(const int &i, const A &pmybuf)
{
	cout << i << endl;
	cout << &pmybuf << endl;	//打印地址
	return;
}

int main()
{
	int mvar = 1;
	int mysecondpar = 12;
	//将mysecond转换成A类对象传递给myprint的第二个参数
	thread mytobj(myprint, mvar, mysecondpar);
	mytobj.join();
	//mytobj.detach();
	cout << "主线程结束" << endl;
	system("pause");
	return 0;
}

运行结果如下:
C++ 并发与多线程学习笔记(三)线程传参隐患 成员函数指针做线程函数_第6张图片
可以看到线程成功运行,现在注释掉join,使用detach。

mytobj.join();
//mytobj.detach();

//更改为

//mytobj.join();
mytobj.detach();

运行结果:
C++ 并发与多线程学习笔记(三)线程传参隐患 成员函数指针做线程函数_第7张图片
主线程结束运行之后,子线程没能成功打印出数据,可以判断这个来自主线程的参数已经被回收了,打印的是空地址。现在我们更改调用线程的方式。

thread mytobj(myprint, mvar, mysecondpar);
//更改为:
thread mytobj(myprint, mvar, A(mysecondpar));

运行结果:
C++ 并发与多线程学习笔记(三)线程传参隐患 成员函数指针做线程函数_第8张图片
可以看到数据成功传入了线程,主线程先执行了含参构造函数创造了一个临时对象,然后在线程中执行了拷贝构造函数,但只调用了一次析构函数则证明可能存在主线程比子线程先结束的现象。

综上所述,多线程程序在运行的时候,可能存在主线程或者其中的函数已经结束等原因导致传入的参数已经被回收,需要这个参数的线程可能因此无法被正确的执行,我们要避免使用已经失效的变量。

结论虽然简单,但非常重要,C++作为底层语言,多线程程序的稳定性决定将来整个系统的稳定性,现实中可能在这基础上要建立庞大的其他系统。

小结

  1. 若传递int这种简单类型参数,建议使用值传递,不要使用引用和指针,防止节外生枝。
  2. 如果传递类对象,避免使用隐式类型转换,可以使用构建临时对象,在函数参数中使用引用可以少使用一次构造函数(避免浪费空间)
  3. 建议不使用detach(),只使用join(),这样不存在局部变量失效对内存非法引用的问题。

线程ID

概念:线程ID是个数字,每个线程(不管是主线程还是子线程)实际上都对应着一个数字,而且每个线程对应的数字都不同,具有和进程控制块、句柄等唯一标识符类似的属性。
使用C++标准库里的函数来获取。
std::this_thread::get_id()
作用是返回当前运行的线程id,包括主线程

线程id可以用来捕获临时对象的构造时机。
我们把线程id和上面的例子结合起来,可以发现,隐式传参的构造函数是在子线程中调用的。用了临时对象后,可以发现所有的A类对象都在主线程中已经构建完毕了。
代码如下:

#include 
#include 
using namespace std;
class A
{
public:
	int m_i;
	A(int a) :m_i(a)
	{
		cout << "A:A(int a)构造函数执行"  << this_thread::get_id() << endl;
	}
	A(const A &a) :m_i(a.m_i)
	{
		cout << "A:A(const A &a)构造函数执行" << this_thread::get_id() << endl;
	}
	~A()
	{
		cout << "析构函数执行" << this_thread::get_id() << endl;
	}
};

void myprint(const A &pmybuf)
{
	cout << &pmybuf << this_thread::get_id() << endl;	//打印地址
	return;
}

int main()
{
	int mvar = 1;
	int mysecondpar = 12;
	//将mysecond转换成A类对象传递给myprint的第二个参数
	thread mytobj(myprint, A(mysecondpar));
	//mytobj.join();
	mytobj.detach();
	
	cout << "主线程结束" << endl;
	//system("pause");
	return 0;
}

传递类对象、智能指针作为线程参数

通过上面的学习,我们可以知道,传递进入子线程的类对象会被拷贝构造,所以我们在函数中修改对象的值不会影响mian函数。
但问题总是源源不断的,我们使用引用的初衷就是为了能够获得返回值,现在既然是假引用,那我们要如何获取这个经过子线程处理过后的值呢?

一个办法是将我们要的值声明为mutable,突破const的限制,这个方法就像友元一样鸡肋,它的使用方法到处都有,这里不赘述。

std::ref函数
用法:

A myobj(10);
thread mytobj(myprint, std::ref(myobj));

结合线程id的特点,通过测试我们可以知道这种方法不再经过拷贝构造,传入的数据是真正的引用。

使用智能指针

先上代码:

void myprint(unique_ptr<int> pzn)
{
	cout << &pzn << this_thread::get_id() << endl;	//打印地址
	return;
}

int main()
{
	int mvar = 1;
	int mysecondpar = 12;
	
	unique_ptr<int> myp(new int(100));
	thread mytobj(myprint, std::move(myp));

	mytobj.join();
	//mytobj.detach();
	
	cout << "主线程结束" << endl;
	//system("pause");
	return 0;
}

智能指针的用法是安全的,主要是需要move,和上面的问题一样,要注意防止线程去调用已经被回收的地址,具体的用法请参照智能指针相关的内容,这里不赘述,它的特点官网有详细的讲解,后续我会专门写关于智能指针的学习笔记。

用成员函数指针做线程函数

上代码:

#include 
#include 
using namespace std;
class A
{
public:
	int m_i;
	A(int a) :m_i(a)
	{
		cout << "A:A(int a)构造函数执行"  << " ,id: " << this_thread::get_id() << endl;
	}
	A(const A &a) :m_i(a.m_i)
	{
		cout << "A:A(const A &a)构造函数执行" << " ,id: " << this_thread::get_id() << endl;
	}
	~A()
	{
		cout << "析构函数执行" << " ,id: " << this_thread::get_id() << endl;
	}

	void thread_work(int n)
	{
		cout << "thread_work执行了" << n << " ,id: " << this_thread::get_id() << endl;
	}
};

int main()
{
	int mvar = 1;
	int mysecondpar = 12;
	
	A a(10);

	thread mytobj(&A::thread_work, a,10);

	mytobj.join();
	//mytobj.detach();
	
	cout << "主线程结束" << endl;
	//system("pause");
	return 0;
}

主要注意用法的不同:
依次为成员函数、类对象、需要传入函数的参数。

thread mytobj(&A::thread_work, a,10);

运行结果:
C++ 并发与多线程学习笔记(三)线程传参隐患 成员函数指针做线程函数_第9张图片
可以尝试,这里的类对象仍可以用ref来引用并操作。
需要注意的是,子线程使用的对象会调用该类的拷贝构造函数。
多线程的内容需要不断积累和总结,内容虽然繁杂,但并不困难,多加练习和与实际应用相结合,便能加深理解。

你可能感兴趣的:(C++多线程学习笔记)