这篇文章将介绍我们在使用detach时如何传参给子线程。
1 detach传参为引用
#include
#include
#include
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的值为拷贝值。
2 detach传参为指针
#include
#include
#include
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使用指针是万万不可的。
3 深度解析字符串传参问题
1)那么有人就会说,既然detach传参为引用也会拷贝,我传字符串时detach的形参写成string的引用不就行了吗?不传string是因为引用也会拷贝,所以避免拷贝两次。
所以继续测试。
#include
#include
#include
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的地址。
buf的地址。
可以看到上面两张图,确实是不同的值,即发生了拷贝。那么这样的字符串传参就确保程序没有问题了吗?实际上这样也并非是安全的。我们知道detach传参引用会进行拷贝,那么什么时候拷贝呢?当主线程结束了,但是这个拷贝并没来得及的话就会造成程序出错。
并且这也引出不仅是字符串,其它类型也一样,只要主线程结束在拷贝之前,程序都是不安全的。
所以继续测试字符串的传参问题。
2)在传字符串引用时,如何防止主线程结束前完成detach的拷贝动作。并且
这里先直接告诉答案:就是主线程传实参时,传匿名对象即可,可以确保主线程结束之前完成detach的拷贝。
下面开始验证:
首先先调用join查看稳定的程序写法,以便对比。
#include
#include
#include
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类型转换构造函数创建的对象地址,最后析构。
把上面程序的join换成detach,并且主线程的打印也去掉,让其快速结束,继续测试。
结果,多运行几次,可以看到,主线程是有可能比拷贝动作先结束的,所以就可能存在危险。
正确方法:实参改变为传临时对象。并且打印对应的线程id。
同理,我们先给出下列代码的join稳定时的现象,方便观察。
#include
#include
#include
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来看,子线程后析构。
由上面的join结论可以得出,传匿名对象时,所有的构造均在主线程结束之前完成,所以将上面的代码join换成detach之后,多次执行,是不怕出现任何问题的,detach结果如下:
所以根据上面三点可以先总结一下detach的传参问题:
4 传进真正的引用进detach时的形参
我们知道,不管join(看上图)还是detach,线程函数的形参即使写成引用形式也是无法获取引用的,照样会拷贝,那么在join时(detach不能传真正的引用)如何传递真正的引用给线程函数使用呢?
那就是std::ref()引用函数了。注意:detach决不能使用该函数,主线程结束后会出错。所以说std::ref()是为join量身定做的。
#include
#include
#include
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;
}
5 传递智能指针作为线程参数
这个没什么好讲的,只是需要注意一点,独占型的智能指针需要使用移动语义进行传参。
#include
#include
#include
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
#include
#include
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;
}
7 大总结detach的传参问题
也就是我能第三点的总结。