c++11并发与多线程【第三节】线程传参详解

第三节 线程传参详解,detach()大坑,成员函数做线程函数

一、传递临时对象作为线程参数(使用detach()之后容易出现的问题)

1.1 要避免的陷阱一

//此时i的引用并没起作用,不是mvar的引用,实际上是值传递,因此detach之后i的值不会被销毁,是安全的,然而pmybuf的地址仍然和mybuf相同,故不安全
void myprint(const int &i,char *pmybuf){//指针在detach子线程时,绝对会有问题
    cout<<i<<endl;
    cout<<pmybuf<<endl;

}
int main(){
    //传递临时对象作为线程参数
    int mvar=1;
    int &mvary=mvar;
    char mybuf[]="this is a test";
    thread mytobj(myprint,mvar,mybuf);
    mytobj.detach();
    cout<<"I love china1"<<endl;
    return 0;
}

输出:

I love china1
1
QUUUU

此时i的引用并没起作用,不是mvar的引用,实际上是值传递,因此detach之后i的值不会随着主线程中的mvar被销毁,是安全的,然而pmybuf的地址仍然和mybuf相同,故不安全。指针在detach子线程时,绝对会有问题。

1.2 要避免的陷阱二

//此时理论上来说,pmybuf的地址和mybuf是不同的,char*会被隐式转换为string,类似与值传递;然而,输出结果有问题
void myprint(const int i,const string &pmybuf){
    cout<<i<<endl;
    cout<<pmybuf.c_str()<<endl;
}
int main(){
    //传递临时对象作为线程参数
    int mvar=1;
    int &mvary=mvar;
    char mybuf[]="this is a test";
    thread mytobj(myprint,mvar,mybuf);//问题在于mybuf是什么时候被转换成string的
    mytobj.detach();
    cout<<"I love china1"<<endl;
    return 0;
}

此时理论上来说,pmybuf的地址和mybuf是不同的,char*会被隐式转换为string,类似与值传递;然而,输出结果有问题。

I love china1
1
RUUUU

问题在于mybuf是什么时候被转换成string的?
从结果来看,会出现mybuf在main函数执行完成后,即主线程完成后,系统才用mybuf去转换为子线程中的string。

如果改成如下写法:

thread mytobj(myprint,mvar,string(mybuf));//直接将mybuf转换成string对象,生成一个临时的string对象

此时可以保证子线程中的值不会出错。这里使用的是string引用,也即是pmybuf和string(mybuf)这个临时对象是同一个地址。那么这个临时对象的创建时间一定会在主线程结束之前吗?

void myprint(const int i,const string &pmybuf){//这里使用的是string引用,也即是pmybuf和string(mybuf)这个临时对象是同一个地址

需要验证这个问题:
首先验证不使用临时对象的情况:

//用于测试构造函数执行时间是否在主线程完成之前
class A{
public:
    int m_i;
    //构造函数
    A(int a):m_i(a){cout<<"A的构造函数执行"<<endl;}
    A(const A &a):m_i(a.m_i){cout<<"A的复制构造函数执行"<<endl;}
    ~A(){cout<<"A的析构函数执行"<<endl;}
};
void myprint(const int i,const A &pmybuf){
    cout<<&pmybuf<<endl;//打印pmybuf对象的地址
}
int main(){
    int mvar=1;
    int mytest=12;
    thread mytobj(myprint,mvar,mytest);
    //mytobj.join();
    mytobj.detach();
    //cout<<"I love china1"<<endl;
    return 0;
}

此时的输出结果中可能会出现什么都无法输出的情况:,这是由于主线程已经执行完毕,而A的构造函数发生在主线程结束之后,mytest已经被销毁,不能提供有效的整形变量来作为构造函数的参数。

其次,验证使用临时对象A(mytest)来作为子线程参数的情况:

//用于测试构造函数执行时间是否在主线程完成之前
class A{
public:
    int m_i;
    //构造函数
    A(int a):m_i(a){cout<<"A的构造函数执行"<<this<<endl;}
    A(const A &a):m_i(a.m_i){cout<<"A的复制构造函数执行"<<this<<endl;}
    ~A(){cout<<"A的析构函数执行"<<this<<endl;}
};
void myprint(const int i,const A &pmybuf){
    cout<<&pmybuf<<endl;//打印pmybuf对象的地址
    return;
}
int main(){
    int mvar=1;
    int mytest=12;
    thread mytobj(myprint,mvar,A(mytest));
    //mytobj.join();
    mytobj.detach();
    //cout<<"I love china1"<<endl;
    return 0;
}

输出结果:
说明在主线程结束之前,mytest被销毁之前,使用mytest构造的对象就已经传入了子线程。从输出结果来看,主线程结束之前,A(mytest)使用构造函数生成了一个临时对象,然后在传递到子线程的过程中又把这个临时对象复制到了其他两个额外生成的对象当中,故:void myprint(const int i,const A &pmybuf) 参数中虽然使用的是A的引用但是实际上并没有生效,而是又额外创建了一个临时的A对象。故:主线程提前于子线程结束并不会导致子线程中的数据被销毁,因为不是同一个数据了。

(备注:老师的使用的是windows+VS stdio ,我使用的是ubuntu +VS Code,老师的输出结果只有一个复制构造函数和对应的析构函数,而我的输出结果有两个复制构造函数和对应的析构,代码一致,猜测可能是编译器不同导致的,倘若我去掉myprint() 函数定义参数中A类对象的引用符&,则会出现3个复制构造函数)

A的构造函数执行0x7fffffffdb88
A的复制构造函数执行0x7fffffffdb40
A的复制构造函数执行0x55555556d2c8
0x55555556d2c8
A的析构函数执行0x55555556d2c8
A的析构函数执行0x7fffffffdb40
A的析构函数执行0x7fffffffdb88

1.3 结论

:在创建线程的同时,构造临时对象的方法传递参数是可行的。只要用临时构造的A类对象作为参数传递给线程,那么就一定能够在主线程执行完毕前把线程函数的第二个参数传递到子线程当中。**

使用detach()应该注意的情况:
(1)若传递int这种简单的类型参数,建议都使用值传递,不要用引用,防止在不同的编译器中出现其他情况。(原因暂时不知)
(2)如果传递类对象,避免隐式类型转换,全部都在创建线程的参数中使用构造临时对象(原因后续知晓),然后在子线程的初始函数的参数中使用引用,否则会在初始函数这又复制出一个临时类对象,浪费内存资源。
(3)建议一般最好不使用detach(),只使用join(),避免main函数中的局部变量失效导致子线程中的使用出错。

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

2.1 线程ID概念

每个线程(主线程和子线程)都有一个不同的ID编号。线程ID可以使用C++标准库中的函数来获取:
std::this_thread::get_id()

2.2 再次测试,带线程ID分析

//用于测试构造函数执行时间是否在主线程完成之前
class A{
public:
    int m_i;
    //构造函数
    A(int a):m_i(a){cout<<"A的构造函数执行"<<this<<" 线程ID= "<<std::this_thread::get_id()<<endl;}
    A(const A& a):m_i(a.m_i){cout<<"A的复制构造函数执行"<<this<<" 线程ID= "<<std::this_thread::get_id()<<endl;}
    ~A(){cout<<"A的析构函数执行"<<this<<" 线程ID= "<<std::this_thread::get_id()<<endl;}
};

void myprint(const A &pmybuf){
    cout<<"子线程myprint的参数地址是:"<<&pmybuf<<" 线程ID= "<<std::this_thread::get_id()<<endl;//打印pmybuf对象的地址
    return;
}
int main(){
    //传递临时对象作为线程参数
    cout<<"主线程ID= "<<std::this_thread::get_id()<<endl;
    int mvar=1;
    int mytest=12;
    thread mytobj(myprint,mytest);
    mytobj.join();
    //mytobj.detach();
    //cout<<"I love china1"<<endl;
    return 0;
}

thread mytobj(myprint,mytest);//此时创建子线程直接使用int类型,而myprint()的参数是一个A对象,所以必然会发生隐式类型转换。
输出:

主线程ID= 140737348159296
A的构造函数执行0x7ffff7a4adf4 线程ID= 140737348155136
子线程myprint的参数地址是:0x7ffff7a4adf4 线程ID= 140737348155136
A的析构函数执行0x7ffff7a4adf4 线程ID= 140737348155136

可以看出,使用mytest来创建A的对象发生在子线程当中,留下了后患,倘若使用的是detach(),那么随着mytest在主线程中的销毁,子线程必然可能会出错或得出错误结果。

若在创建子线程是使用临时对象作为参数:

int main(){
    //传递临时对象作为线程参数
    cout<<"主线程ID= "<<std::this_thread::get_id()<<endl;
    int mvar=1;
    int mytest=12;
    thread mytobj(myprint,A(mytest));
    mytobj.join();
    //mytobj.detach();
    //cout<<"I love china1"<<endl;
    return 0;
}

输出:

主线程ID= 140737348159296
A的构造函数执行0x7fffffffdb84 线程ID= 140737348159296
A的复制构造函数执行0x7fffffffdb40 线程ID= 140737348159296
A的复制构造函数执行0x55555556d2c8 线程ID= 140737348159296
子线程myprint的参数地址是:0x55555556d2c8 线程ID= 140737348155136
A的析构函数执行0x55555556d2c8 线程ID= 140737348155136
A的析构函数执行0x7fffffffdb40 线程ID= 140737348159296
A的析构函数执行0x7fffffffdb84 线程ID= 140737348159296

可以看出,此时创建线程的临时对象参数的创建,和临时的复制构造类对象都是在主线程当中,主线程的结束不会影响到子线程。

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

(1)传递类对象:

//用于测试构造函数执行时间是否在主线程完成之前
class A{
public:
    mutable int m_i;//mutable使得m_i在任何情况下都能修改
    //构造函数
    A(int a):m_i(a){cout<<"A的构造函数执行"<<this<<" 线程ID= "<<std::this_thread::get_id()<<endl;}
    A(const A& a):m_i(a.m_i){cout<<"A的复制构造函数执行"<<this<<" 线程ID= "<<std::this_thread::get_id()<<endl;}
    ~A(){cout<<"A的析构函数执行"<<this<<" 线程ID= "<<std::this_thread::get_id()<<endl;}
};

void myprint(const A &pmybuf){
    pmybuf.m_i=199;//修改该值不会影响到main函数
    cout<<"子线程myprint的参数地址是:"<<&pmybuf<<" 线程ID= "<<std::this_thread::get_id()<<endl;//打印pmybuf对象的地址
    return;
}
int main(){
    cout<<"主线程ID= "<<std::this_thread::get_id()<<endl;
    A myobj(10);//生成类对象,使用类对象作为线程参数
    thread mytobj(myprint,myobj);
    mytobj.join();
    //mytobj.detach();
    //cout<<"I love china1"<<endl;
    return 0;
}

输出:

主线程ID= 140737348151104
A的构造函数执行0x7fffffffdb84 线程ID= 140737348151104
A的复制构造函数执行0x7fffffffdb40 线程ID= 140737348151104
A的复制构造函数执行0x55555556d2c8 线程ID= 140737348151104
子线程myprint的参数地址是:0x55555556d2c8 线程ID= 140737348146944
A的析构函数执行0x55555556d2c8 线程ID= 140737348146944
A的析构函数执行0x7fffffffdb40 线程ID= 140737348151104
A的析构函数执行0x7fffffffdb84 线程ID= 140737348151104

考虑以上这种情况,在A类中使成员变量m_i变成可修改的,并且在myprint线程函数中对m_i进行修改,且使用的是引用,然而结果却是只能修改到子线程中的成员变量m_i,不会影响到主线程中的对象参数myobj对象中的m_i。因为编译器出于安全考虑,不管函数参数使用的是否是引用,都统一使用值传递的方式生成一个临时对象。
这显然是不符合预期的,达不到在子线程中 ,修改主线程中对象参数成员变量的目的。如何解决这个问题呢?

解决:使用std::ref()函数: 使用ref告诉编译器,就是要把myobj的引用传递到子线程当中

int main(){
    cout<<"主线程ID= "<<std::this_thread::get_id()<<endl;
    A myobj(10);//生成类对象,使用类对象作为线程参数
    thread mytobj(myprint,std::ref(myobj));//使用ref告诉编译器,就是要把myobj的引用传递到子线程当中
    mytobj.join();
    //mytobj.detach();
    //cout<<"I love china1"<<endl;
    return 0;
}

此时:子线程的初始函数myprint()就可以改成:可以不在参数前使用const;

void myprint(A &pmybuf){
    pmybuf.m_i=199;//修改该值不会影响到main函数
    cout<<"子线程myprint的参数地址是:"<<&pmybuf<<" 线程ID= "<<std::this_thread::get_id()<<endl;//打印pmybuf对象的地址
    return;
}

A类定义成员变量也可以去掉mutable;

class A{
public:
    int m_i;//mutable使得m_i在任何情况下都能修改
    //构造函数
    A(int a):m_i(a){cout<<"A的构造函数执行"<<this<<" 线程ID= "<<std::this_thread::get_id()<<endl;}
    A(const A& a):m_i(a.m_i){cout<<"A的复制构造函数执行"<<this<<" 线程ID= "<<std::this_thread::get_id()<<endl;}
    ~A(){cout<<"A的析构函数执行"<<this<<" 线程ID= "<<std::this_thread::get_id()<<endl;}
};

输出:不会再出现复制构造函数再创建一个临时对象了;且子线程中的对象与主线程中传入的参数地址一致,确实传入了引用;最终主线程中的成员变量成功地被子线程进行了修改;

主线程ID= 140737348151104
A的构造函数执行0x7fffffffdb7c 线程ID= 140737348151104
子线程myprint的参数地址是:0x7fffffffdb7c 线程ID= 140737348146944
主线程中的myobj的m_i=:199
A的析构函数执行0x7fffffffdb7c 线程ID= 140737348151104

(2)传递智能指针:

void myprint(unique_ptr<int> pmybuf){
    cout<<"子线程myprint  "<<" 线程ID= "<<std::this_thread::get_id()<<endl;
    return;
}
int main(){

    cout<<"主线程ID= "<<std::this_thread::get_id()<<endl;
    unique_ptr<int> myp(new int(100));
    thread mytobj(myprint,myp);
    mytobj.join();
    //mytobj.detach();
    //cout<<"I love china1"<<endl;
    return 0;
}

此时编译器会报错:不能转换
在这里插入图片描述解决方法:使用std:move()

int main(){

    cout<<"主线程ID= "<<std::this_thread::get_id()<<endl;
    unique_ptr<int> myp(new int(100));
    thread mytobj(myprint,std::move(myp));
    mytobj.join();
    //mytobj.detach();
    //cout<<"I love china1"<<endl;
    return 0;
}

此时程序中,主线程中的myp的内存地址与子线程中的pmybuf内存地址相同,执行完std:move之后,智能指针所指向的对象移动到子线程的pmybuf中,由于使用的是join(),当子线程执行完毕后,智能指针pmybuf就被释放了。(智能指针这一块我不熟,所以直接复述原话了)

如果使用的是detach(),倘若主线程先于子线程执行完毕,智能指针myp已经被释放了,子线程中的pmybuf就会指向一个不可预知的内存区域,就会出问题。所以不能使用detach()

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

3.1 使用类中的任意成员函数作为线程函数

class A{
public:
    int m_i;
    //构造函数
    A(int a):m_i(a){cout<<"A的构造函数执行"<<this<<" 线程ID= "<<std::this_thread::get_id()<<endl;}
    A(const A& a):m_i(a.m_i){cout<<"A的复制构造函数执行"<<this<<" 线程ID= "<<std::this_thread::get_id()<<endl;}
    ~A(){cout<<"A的析构函数执行"<<this<<" 线程ID= "<<std::this_thread::get_id()<<endl;}

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

int main(){
    cout<<"主线程ID= "<<std::this_thread::get_id()<<endl;
    A myobj(10);
    thread mytobj(&A::thread_work,myobj,15);
    mytobj.join();
    //mytobj.detach();
    return 0;
}

输出:此时传入子线程的是复制构造的临时对象,可以使用join(),也可以使用detach()

主线程ID= 140737348151104
A的构造函数执行0x7fffffffdb70 线程ID= 140737348151104
A的复制构造函数执行0x7fffffffdb34 线程ID= 140737348151104
A的复制构造函数执行0x55555556d2cc 线程ID= 140737348151104
子线程thread_work执行:0x55555556d2cc 线程ID=140737348146944
A的析构函数执行0x55555556d2cc 线程ID= 140737348146944
A的析构函数执行0x7fffffffdb34 线程ID= 140737348151104
A的析构函数执行0x7fffffffdb70 线程ID= 140737348151104

但是如果 是thread mytobj(&A::thread_work,std::ref(myobj),15);//
或者 thread mytobj(&A::thread_work,&myobj,15);//
则传入子线程中的就是myobj的引用,此时不能使用detach().

3.2 使用重载函数作为线程参数

class A{
public:
    int m_i;//mutable使得m_i在任何情况下都能修改
    //构造函数
    A(int a):m_i(a){cout<<"A的构造函数执行"<<this<<" 线程ID= "<<std::this_thread::get_id()<<endl;}
    A(const A& a):m_i(a.m_i){cout<<"A的复制构造函数执行"<<this<<" 线程ID= "<<std::this_thread::get_id()<<endl;}
    ~A(){cout<<"A的析构函数执行"<<this<<" 线程ID= "<<std::this_thread::get_id()<<endl;}

    void thread_work(int num){
        cout<<"子线程thread_work执行:"<<this<<"   线程ID="<<std::this_thread::get_id()<<endl;
    }
    void operator()(int num)//重载()
    {
        cout<<"子线程()执行:"<<this<<"   线程ID="<<std::this_thread::get_id()<<endl;
    }
};

int main(){
    cout<<"主线程ID= "<<std::this_thread::get_id()<<endl;
    A myobj(10);
    thread mytobj(myobj,15);
    mytobj.join();
    //mytobj.detach();
    return 0;
}

主线程ID= 140737348151104
A的构造函数执行0x7fffffffdb88 线程ID= 140737348151104
A的复制构造函数执行0x7fffffffdb54 线程ID= 140737348151104
A的复制构造函数执行0x55555556d2cc 线程ID= 140737348151104
A的析构函数执行0x7fffffffdb54 线程ID= 140737348151104
子线程()执行:0x55555556d2cc 线程ID=140737348146944
A的析构函数执行0x55555556d2cc 线程ID= 140737348146944
A的析构函数执行0x7fffffffdb88 线程ID= 140737348151104

结论子线程中用到主线程中变量的引用或指针的情况,不能使用detach()

文章内容来源《C++并发与多线程视频课程》

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2022-2024 lk
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信