C++11并发与多线程笔记(3)线程传参详解,detach()大坑,成员函数做线程函数

1、传递临时参数作为线程参数

1.1、要避免的陷阱(解释1)

示例代码:

#include <iostream>
#include <thread>
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.join();

	cout << "I love China!" << endl;

	return 0;
}

对上方代码进行Debug,变量mvar的地址为0x012ffa34,它的引用mvary的地址也是0x012ffa34,而传到myprint函数里的i的地址却是0x015d1054,也就是说myprint函数的形参i不是一个真引用,而是复制了一个mvar,对复制的mvar做了一个引用给了形参i,实际上是一个值传递。

  • 分析认为,i并不是mvar的引用,实际是值传递,那么即便主线程中对子线程使用了detach(),那么子线程中的i值也是安全的。不过还是建议使用值传递,即形参为const int i,而不是const int& i

1.2、要避免的陷阱(解释2)

示例代码:

#include <iostream>
#include <thread>
using namespace std;

//void myprint(const int i, char* pmybuf)
void myprint(const int i, const string& 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.join();

	cout << "I love China!" << endl;

	return 0;
}

myprint函数第二个参数类型为char*时,通过Debug得到,main函数中,mybuf首地址为0x0118fb0cmyprint函数中的形参pmybuf的首地址也为0x0118fb0c,这样的话,当主线程对子线程使用detach()时,主线程如果先结束,mybuf内存就会被释放,pmybuf对该内存的访问可能会出错,出现不可预想的结果。

myprint函数第二个参数类型为const string&时,通过Debug得到,main函数中,mybuf首地址为0x00fef79c,而myprint函数中形参pmybuf的首地址为0x0196f824,也就是说变量mvar传入myprint函数进行了隐式转换,而myprint函数中的pmybuf是对char[]类型的变量隐式转换成string类型而产生的临时变量的引用,那么问题来了,是什么时候进行的隐式转换呢?主线程中?还是子线程中?

**答:**事实上,主线程执行完了,mybuf都被回收了,系统才用mybuf去隐式转换为string

  • 我们对代码进行改进并进行求证,如下:

    #include <iostream>
    #include <thread>
    using namespace std;
    
    class A
    {
    public:
    	int m_i;
    
    	//类型转换构造函数,可以把一个int转换成一个类A对象
    	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 << "[A::~A()析构函数执行]" << endl;
    	}
    };
    
    void myprint(const int i, const A& pmybuf)
    {
    	cout << &pmybuf << endl;		//这里打印的是pmybuf对象的地址
    	return;
    }
    
    int main()
    {
    	int mvar = 1;
    	int mysecondpar = 12;
    	thread mytobj(myprint, mvar, mysecondpar);	//将mysecondpar转成A类型对象传递给myprint的第二个参数
    	
    	//mytobj.join();
    	mytobj.detach();
    
    	cout << "I love China!" << endl;
    
    	return 0;
    }
    
  • 当我们使用join()的时候,可以看到如下结果:

    image-20211030132459133

    说明myprint这个函数的的pmybuf确实能通过mysecondpar进行类型转换构造出一个A类对象并获得该对象的引用。

  • 但当我们使用detach()时,却看到了如下结果:

    image-20211030132944212

    说明通过mysecondpar构造的A类对象在主线程结束之前根本就没有构造出来。这样的话,主线程结束后,mysecondpar的内存空间就已经被回收了,那么再用无效的变量去构造一个A类对象,肯定会有问题,会导致一些未定义的行为。

  • 我们再次改进我们的代码,如下:

    #include <iostream>
    #include <thread>
    using namespace std;
    
    class A
    {
    public:
    	int m_i;
    
    	//类型转换构造函数,可以把一个int转换成一个类A对象
    	A(int a) :m_i(a)
    	{
    		cout << "[A::A(int a)构造函数执行]" << this << endl;
    	}
    
    	A(const A& a) :m_i(a.m_i)
    	{
    		cout << "[A::A(const A& a)拷贝构造函数执行]" << this << endl;
    	}
    
    	~A()
    	{
    		cout << "[A::~A()析构函数执行]" << this << endl;
    	}
    };
    
    void myprint(const int i, const A& pmybuf)
    {
    	cout << &pmybuf << endl;		//这里打印的是pmybuf对象的地址
    	return;
    }
    
    int main()
    {
    	int mvar = 1;
    	int mysecondpar = 12;
    	thread mytobj(myprint, mvar, A(mysecondpar));	//将mysecondpar转成A类型对象传递给myprint的第二个参数
    	
    	//mytobj.join();
    	mytobj.detach();
    
    	cout << "I love China!" << endl;
    
    	return 0;
    }
    
  • 可以看到我们在37行对mysecondpar进行了一个强制类型转换,构造了一个临时的A类对象,可以看到如下结果:

    image-20211030140203936

    这样的话,只要用临时构造的A类对象作为参数传递给子线程,那么就一定能够在主线程执行完毕前把子线程函数的第二个参数构建出来,即便使用了detach(),子线程也能安全运行。

    证明请往下看第2部分。

1.3、总结

  1. 若传递int这种简单数据类型参数,建议都是值传递,不要用引用,防止节外生枝。
  2. 如果传递类对象,避免隐式转换。最好都在创建子线程这一行就构建出临时对象来,然后在子线程的函数参数里,用引用来接,否则系统还会多构造一次类对象,造成浪费。
  3. 建议少使用或者尽量不使用detach(),只使用join(),这样就不存在局部变量失效导致线程对内存的非法引用问题。

2、临时对象作为线程参数的测试方法

2.1 线程id

  • 线程id概念:id是一个数字,每个线程(不管是主线程,还是子线程)实际上都对应着一个id,每个线程对应的id不同。

  • 线程id可以用c++标准库里的函数来获取 -> std::this_thread::get_id()

2.2 临时对象构造时机抓捕

示例代码:

#include <iostream>
#include <thread>
using namespace std;

class A
{
public:
	int m_i;

	//类型转换构造函数,可以把一个int转换成一个类A对象
	A(int a) :m_i(a)
	{
		cout << "[A::A(int a)构造函数执行]" << this << " threadid = " << std::this_thread::get_id() << endl;
	}

	A(const A& a) :m_i(a.m_i)
	{
		cout << "[A::A(const A& a)拷贝构造函数执行]" << this << " threadid = " << std::this_thread::get_id() << endl;
	}

	~A()
	{
		cout << "[A::~A()析构函数执行]" << this << " threadid = " << std::this_thread::get_id() << endl;
	}
};

void myprint(const A& pmybuf)
{
	cout << "子线程myprint的参数地址为:" << & pmybuf << " threadid = " << std::this_thread::get_id() << endl;
	return;
}

int main()
{
	cout << "主线程id是:" << std::this_thread::get_id() << endl;

	int mvar = 1;
	std::thread mytobj(myprint, mvar);
	//std::thread mytobj(myprint, A(mvar);
	
	mytobj.join();
	//mytobj.detach();

	cout << "I love China!" << endl;

	return 0;
}
  • 当使用join()时,所得到的结果如下:

    image-20211030143950071

    说明myprint函数的参数pmybuf的A类对象是在子线程中进行构造的,也就是说mvar隐式转换成A类对象是在子线程中进行构造的,那当使用detach()时,主线程如果先结束,mvar的内存空间就会被回收,子线程在一个无效的变量基础上进行类型的隐式转换,将会出现不可预料的错误!

  • 当我们将第38行代码进行注释,第39行代码取消注释,即在创建线程时就创建临时对象,得到结果如下:

    image-20211030144616395

    结果说明,当用了临时对象后,所有的A类对象都在main()函数中就已经构建完毕了。当我们将join()改为detach()后,得到的结果如下:

    image-20211030144914050

    结果说明,即使使用了detach(),主线程执行完毕,也不影响子线程的正常执行,因为子线程所需要的A类对象在主线程结束之前就已经构造了出来。

    注:构造函数是主线程中临时对象的构造函数,拷贝构造函数则是传递给子线程的类对象,这个拷贝构造函数是thread内部自动帮我们进行的,并传递到子线程;第一个析构函数是主线程中临时对象的析构,第二个析构则是子线程中函数传参对象的析构。

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

3.1 传递类对象

示例代码:

#include <iostream>
#include <thread>
using namespace std;

class A
{
public:
	mutable int m_i;

	//类型转换构造函数,可以把一个int转换成一个类A对象
	A(int a) :m_i(a)
	{
		cout << "[A::A(int a)构造函数执行]" << this << " threadid = " << std::this_thread::get_id() << endl;
	}

	A(const A& a) :m_i(a.m_i)
	{
		cout << "[A::A(const A& a)拷贝构造函数执行]" << this << " threadid = " << std::this_thread::get_id() << endl;
	}

	~A()
	{
		cout << "[A::~A()析构函数执行]" << this << " threadid = " << std::this_thread::get_id() << endl;
	}
};

void myprint(const A& pmybuf)	//传入的为临时变量具有不可修改性质,必须加const
{
	pmybuf.m_i = 199;	//我们修改该值,不会影响到main()函数
	cout << "子线程myprint的参数地址为:" << & pmybuf << " threadid = " << std::this_thread::get_id() << endl;
	return;
}

int main()
{
	A myobj(10);	//生成一个类对象
	std::thread mytobj(myprint, myobj);		//将类对象作为线程参数
	
	mytobj.join();
	//mytobj.detach();

	//cout << "I love China!" << endl;

	return 0;
}
  • 当我们使用join()时,运行结果如下:

    image-20211030152550332

我们对上方代码进行Debug发现,传入到子线程中的pmybuf的地址为0x01162540,而主线程中的myobj的地址是0x00bffe84,子线程中myprint函数的形参引用并没有起作用,实际上还是利用了拷贝构造构造了一个A类对象进行传参,因此对myobj也并没有影响。那么问题来了,怎样将真正的myobj传参到子线程中减免浪费呢?接下来就轮到std:ref()出场了。

3.2 std:ref()

修改上方代码:

#include <iostream>
#include <thread>
using namespace std;

class A
{
public:
	int m_i;

	//类型转换构造函数,可以把一个int转换成一个类A对象
	A(int a) :m_i(a)
	{
		cout << "[A::A(int a)构造函数执行]" << this << " threadid = " << std::this_thread::get_id() << endl;
	}

	A(const A& a) :m_i(a.m_i)
	{
		cout << "[A::A(const A& a)拷贝构造函数执行]" << this << " threadid = " << std::this_thread::get_id() << endl;
	}

	~A()
	{
		cout << "[A::~A()析构函数执行]" << this << " threadid = " << std::this_thread::get_id() << endl;
	}
};

void myprint(A& pmybuf)
{
	pmybuf.m_i = 199;	//我们修改该值,不会影响到main()函数
	cout << "子线程myprint的参数地址为:" << & pmybuf << " threadid = " << std::this_thread::get_id() << endl;
	return;
}

int main()
{
	A myobj(10);	//生成一个类对象
	std::thread mytobj(myprint, std::ref(myobj));		//将类对象作为线程参数
	
	mytobj.join();
	//mytobj.detach();

	//cout << "I love China!" << endl;

	return 0;
}
  • 当我们使用join()时,代码结果如下:

    image-20211030154118881

    可以看到,已经没有再调用拷贝构造函数,通过Debug可以看到myobj最初值为10,地址为0x006ffb34,传递到子线程函数中pmybuf的值为10,地址为0x006ffb34,经过子线程函数的作用,子线程函数中pmybuf的值变为199,地址为0x006ffb34,myobj的值也变为199,地址为0x006ffb34

    说明这次是真正的将myobj传参到子线程中。

3.3 传递智能指针

示例代码:

#include <iostream>
#include <thread>
using namespace std;

class A
{
public:
	int m_i;

	//类型转换构造函数,可以把一个int转换成一个类A对象
	A(int a) :m_i(a)
	{
		cout << "[A::A(int a)构造函数执行]" << this << " threadid = " << std::this_thread::get_id() << endl;
	}

	A(const A& a) :m_i(a.m_i)
	{
		cout << "[A::A(const A& a)拷贝构造函数执行]" << this << " threadid = " << std::this_thread::get_id() << endl;
	}

	~A()
	{
		cout << "[A::~A()析构函数执行]" << this << " threadid = " << std::this_thread::get_id() << endl;
	}
};

void myprint(unique_ptr<int> pzn)
{
	return;
}

int main()
{
	unique_ptr<int> myp(new int(100));
	std::thread mytobj(myprint, std::move(myp));	//独占式智能指针必须通过std::move进行传参(转移)
	
	mytobj.join();
	//mytobj.detach();

	//cout << "I love China!" << endl;

	return 0;
}
  • 在使用join()的情况下,对上方代码进行Debug,可以看到,最初的myp所指向的地址为0x01290a30,指向的值为100,传递到子线程中的智能指针pzn指向的地址也为0x01290a30,指向的值为100,传递到子线程后,myp就变为了empty
  • 如果使用detach(),那么如果主线程执行完毕,myp被回收,而子线程还未结束或开始,子线程函数中的pzn指向的是被系统回收的一块内存,那么就将出现不可预料的问题。
  • 所以在传递智能指针时,只能使用join(),不能使用detach()

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

示例代码:

#include <iostream>
#include <thread>
using namespace std;

class A
{
public:
	int m_i;

	//类型转换构造函数,可以把一个int转换成一个类A对象
	A(int a) :m_i(a)
	{
		cout << "[A::A(int a)构造函数执行]" << this << " threadid = " << std::this_thread::get_id() << endl;
	}

	A(const A& a) :m_i(a.m_i)
	{
		cout << "[A::A(const A& a)拷贝构造函数执行]" << this << " threadid = " << std::this_thread::get_id() << endl;
	}

	~A()
	{
		cout << "[A::~A()析构函数执行]" << this << " threadid = " << std::this_thread::get_id() << endl;
	}

	void thread_work(int num)
	{
		cout << "[子线程thread_work执行]" << this << " threadid = " << std::this_thread::get_id() << endl;
	}
};

int main()
{
	A myobj(10);
	std::thread mytobj(&A::thread_work, myobj, 15);
    //std::thread mytobj(&A::thread_work, std::ref(myobj), 15);
    //std::thread mytobj(&A::thread_work, &myobj, 15);
	
	mytobj.join();	
	//mytobj.detach();

	//cout << "I love China!" << endl;

	return 0;
}

使用join()代码执行结果如下:

image-20211030162246585

结果表明:thread是利用拷贝构造函数拷贝构造了一个myobj传递给了子线程,因此可以使用detach(),因为是在主线程结束之前就把拷贝构造出来的A类对象传递到了子线程。在主线程中进行拷贝构造A类对象,在子线程中对拷贝构造出来的A类对象进行析构。

如果将35行代码换成36行或者37行代码,及对myobj进行引用,这样的话就不能使用detach(),因为如果主线程先结束,myobj内存空间被回收,子线程则会对被回收的变量进行引用,即指向内存泄露的地方,会出现不可预料的后果。

3.5 对传递类对象进行改造

示例代码:

#include <iostream>
#include <thread>
using namespace std;

class A
{
public:
	int m_i;

	//类型转换构造函数,可以把一个int转换成一个类A对象
	A(int a) :m_i(a)
	{
		cout << "[A::A(int a)构造函数执行]" << this << " threadid = " << std::this_thread::get_id() << endl;
	}

	A(const A& a) :m_i(a.m_i)
	{
		cout << "[A::A(const A& a)拷贝构造函数执行]" << this << " threadid = " << std::this_thread::get_id() << endl;
	}

	~A()
	{
		cout << "[A::~A()析构函数执行]" << this << " threadid = " << std::this_thread::get_id() << endl;
	}

	void thread_work(int num)
	{
		cout << "[子线程thread_work执行]" << this << " threadid = " << std::this_thread::get_id() << endl;
	}

	void operator()(int num)
	{
		cout << "[子线程()执行]" << this << " threadid = " << std::this_thread::get_id() << endl;
	}
};

int main()
{
	A myobj(10);
	std::thread mytobj(myobj, 15);
    //std::thread mytobj(std::ref(myobj), 15);	//不能使用&myobj,因为&myobj是取址,取得是this指针地址,而std::ref(myobj)是对myobj的引用
	
	mytobj.join();
	//mytobj.detach();

	//cout << "I love China!" << endl;

	return 0;
}

结果如下:

image-20211030163952651

结果表明,当不使用引用时还是内不通过拷贝构造函数传参,因此可以使用detach()

当使用引用时,即第40行代码改为第11行代码,结果如下:

image-20211030164148306

结果表明,此时就会减少一次构造拷贝函数,提升运行速度,但同时也不能使用detach(),否则会同前面讲的一样因为内存泄漏出现不可预料的结果。

3.6 总结

  1. 在使用detach()时,不要使用引用,否则会造成内存泄漏。
  2. 在使用join()时,传递类对象最好使用引用,这样会减少拷贝构造次数,提升运行速度。
  3. 在使用引用时最好使用std::ref(),而不是&。

注:本人学习c++多线程视频地址:C++多线程学习地址

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值