02C++11多线程编程之detach传参详解

02C++11多线程编程之detach传参详解

这篇文章将介绍我们在使用detach时如何传参给子线程。

1 detach传参为引用

#include<iostream>
#include<thread>
#include<string>

using namespace std;


void myprint1(const int &i){

	//打印i的地址看是否是引用
	cout << &i << endl;

	cout << i << endl;

}

int main(){

	int mvar = 1;
	cout << &mvar << endl;

	thread myobj(myprint1, mvar);

	myobj.detach();


	cout << "主线程执行!" << endl;

	return 0;
}

结果分析:
为了防止主线程结束导致detach的子线程无法输出,我们在主线程打个断点或者睡眠一下,结果可以看到,上面代码即使是传引用,形参i的值为拷贝值。
02C++11多线程编程之detach传参详解

2 detach传参为指针

#include<iostream>
#include<thread>
#include<string>

using namespace std;


void myprint1(char *p){

	cout << p << endl;

}

int main(){

	char buf[] = "I am bb";

	thread myobj(myprint1, buf);

	myobj.detach();


	cout << "主线程执行!" << endl;

	return 0;
}

结果:由于打印地址首地址会直接输出字符串,所以我们截图说明。可以看到,detach传参为地址时,不会产生拷贝,所以detach使用指针是万万不可的。
02C++11多线程编程之detach传参详解

3 深度解析字符串传参问题
1)那么有人就会说,既然detach传参为引用也会拷贝,我传字符串时detach的形参写成string的引用不就行了吗?不传string是因为引用也会拷贝,所以避免拷贝两次。
所以继续测试。

#include<iostream>
#include<thread>
#include<string>

using namespace std;

//detach线程的参数必须加sonst,否则编译器会让你报错
void myprint1(const string &p){

	cout << p << endl;

}

int main(){

	char buf[] = "I am bb";

	thread myobj(myprint1, buf);

	myobj.detach();


	cout << "主线程执行!" << endl;

	return 0;
}

结果:
右击p选择快速监视,获取string的地址。
02C++11多线程编程之detach传参详解
buf的地址。
02C++11多线程编程之detach传参详解

可以看到上面两张图,确实是不同的值,即发生了拷贝。那么这样的字符串传参就确保程序没有问题了吗?实际上这样也并非是安全的。我们知道detach传参引用会进行拷贝,那么什么时候拷贝呢?当主线程结束了,但是这个拷贝并没来得及的话就会造成程序出错。
并且这也引出不仅是字符串,其它类型也一样,只要主线程结束在拷贝之前,程序都是不安全的。
所以继续测试字符串的传参问题。

2)在传字符串引用时,如何防止主线程结束前完成detach的拷贝动作。并且
这里先直接告诉答案:就是主线程传实参时,传匿名对象即可,可以确保主线程结束之前完成detach的拷贝。

下面开始验证:
首先先调用join查看稳定的程序写法,以便对比。

#include<iostream>
#include<thread>
#include<string>

using namespace std;

class A{
public:
	int m_i;

public:
	//类型转换构造函数,可以把一个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(A &a)复制构造函数执行!" << endl; }

	~A() { cout << "A::~A()析构函数执行!" << endl; }
};

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

int main(){

	int mvar = 1;
	int mysecondpar = 12;

	thread myobj(myprint, mvar, mysecondpar);//我们希望mysecondpar转成A类型对象传递给myprint的第二个参数

	if (myobj.joinable()) {
		myobj.join();
		//myobj.detach();
	}

	cout << "主线程执行!" << endl;

	return 0;
}

结果:我们可以看到结果是正常的,子线程首先调用拷贝构造,然后打印用int类型转换构造函数创建的对象地址,最后析构。
02C++11多线程编程之detach传参详解

把上面程序的join换成detach,并且主线程的打印也去掉,让其快速结束,继续测试。
结果,多运行几次,可以看到,主线程是有可能比拷贝动作先结束的,所以就可能存在危险。
02C++11多线程编程之detach传参详解

正确方法:实参改变为传临时对象。并且打印对应的线程id。
同理,我们先给出下列代码的join稳定时的现象,方便观察。

#include<iostream>
#include<thread>
#include<string>

using namespace std;

class A{
public:
	int m_i;

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

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

void myprint2(const A &pmybuf){

	cout << "子对象myprint的参数地址是" << &pmybuf << " threadid" << std::this_thread::get_id() << endl;// 打印的是pmybuf对象的地址

}

int main(){

	cout << "主线程id:" << std::this_thread::get_id() << endl;

	int  mvar = 2;
	//thread myobj(myprint2, mvar);  //致命问题是在子线程中构造A类对象
	thread myobj(myprint2, A(mvar)); //用了临时对象后,所有的A类对象都在main()函数中已经构造完毕了,故detach不再惧怕主线程先结束
	if (myobj.joinable()) {
		myobj.join();
		//myobj.detach();
	}
 

	return 0;
}

以下是join的稳定代码输出结果,传参为匿名对象时,所有的对象拷贝均在主线程中完成(注意不是匿名对象是在子线程中构造),后面的析构因为竞争打印得比较乱,但是根据threadid来看,子线程后析构。
02C++11多线程编程之detach传参详解

由上面的join结论可以得出,传匿名对象时,所有的构造均在主线程结束之前完成,所以将上面的代码join换成detach之后,多次执行,是不怕出现任何问题的,detach结果如下:
02C++11多线程编程之detach传参详解

所以根据上面三点可以先总结一下detach的传参问题:

  • 1)int型的直接值传递。
  • 2)指针绝不使用。
  • 3)传递类对象时,形参为引用,实参为匿名对象。形参为引用是防止多次拷贝,实参为匿名对象是防止子线程拷贝之前主线程提前结束。
  • 4)使用join上面问题均不出现。

4 传进真正的引用进detach时的形参
我们知道,不管join(看上图)还是detach,线程函数的形参即使写成引用形式也是无法获取引用的,照样会拷贝,那么在join时(detach不能传真正的引用)如何传递真正的引用给线程函数使用呢?
那就是std::ref()引用函数了。注意:detach决不能使用该函数,主线程结束后会出错。所以说std::ref()是为join量身定做的。

#include<iostream>
#include<thread>
#include<string>

using namespace std;

class A{
public:
	int m_i;
	//mutable int m_i;由于在C++11线程回调的函数中,形参为类对象时需要加上const才能确保语法正确,这样就不能修改到该对象值了,添加mutable即可。
public:
	//类型转换构造函数,可以把一个int转换成一个类A对象。
	A(int a) :m_i(a){
		cout << "A::A(int a)构造函数执行!" << " this=" << this << " threadid:" << std::this_thread::get_id() << endl;
	}
	A(const A &a) :m_i(a.m_i){
		cout << "A::A(A &a)复制构造函数执行!" << " this=" << this << " threadid:" << std::this_thread::get_id() << endl;
	}

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

void myprint2(const A &pmybuf){

	cout << "子对象myprint的参数地址是" << &pmybuf << " threadid" << std::this_thread::get_id() << endl;// 打印的是pmybuf对象的地址

}

int main(){

	cout << "主线程id:" << std::this_thread::get_id() << endl;

	A mvar(2);
	thread myobj(myprint2, ref(mvar));//使join获取真正的引用,为join量身定做
	if (myobj.joinable()) {
		myobj.join();
		//myobj.detach();//决不能使用detach
	}
 

	return 0;
}

结果可以看到,构造函数只执行了一次。
02C++11多线程编程之detach传参详解

5 传递智能指针作为线程参数
这个没什么好讲的,只是需要注意一点,独占型的智能指针需要使用移动语义进行传参。

#include<iostream>
#include<thread>
#include<string>

using namespace std;

void myprint3(unique_ptr<int> pzn){
	cout << "unique_ptr 传参" << endl;
}

int main(){

	unique_ptr<int> myp(new int(100));
	thread myobj(myprint3, std::move(myp));//unique_ptr为独占型智能指针,不使用move传参会编译报错,凡是unique_pte传参都需要这样
	if (myobj.joinable()) {
		myobj.join();
		//myobj.detach();//决不能使用detach,因为new的内存在主线程中,虽然一开始共用同一进程的堆,
		//但是主线程结束后,子线程被系统的某个进程回收,共享堆改变,会出现未知问题
	}

	return 0;
}

6 用成员函数指针做线程函数
成员函数作线程函数时,写法比较奇怪。平常的普通回调函数直接传函数名即可,而成员函数需要取地址符并且声明是那个类,后面为形参,这里注意:由于成员函数有this指针,所以传成员函数的第二个参数需为该类对象地址。直接看代码。

#include<iostream>
#include<thread>
#include<string>

using namespace std;

class A{
public:
	mutable int m_i;

public:
	//类型转换构造函数,可以把一个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(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);
	//thread mytobj(&A::thread_work, myobj, 15);
	thread mytobj(&A::thread_work, &myobj, 15);//join时参2可以使用地址传参,让其只构造一次。也可调用ref
	//当成员函数为括号重载时可以这样写thread mytobj(myobj, 15);或者thread mytobj(std::ref(myobj), 15);但thread mytobj(&myobj, 15);编译器让你报错
	mytobj.join();

	return 0;
}

结果搞定,成功运行。
02C++11多线程编程之detach传参详解

7 大总结detach的传参问题
也就是我能第三点的总结。

  • 1)int型的直接值传递。
  • 2)指针绝不使用。
  • 3)传递类对象时,形参为引用,实参为匿名对象。形参为引用是防止多次拷贝,实参为匿名对象是防止子线程拷贝之前主线程提前结束。
  • 4)使用join上面问题均不出现。
上一篇:C++并发与多线程学习笔记--参数传递详解


下一篇:this:从JavaScript执行上下文视角讲this