https://blog.csdn.net/u012507022/article/details/85845799
https://blog.csdn.net/u014571489/article/details/82988922
陷阱1:子线程传递参数避免使用指针
先来看一段代码:
代码乍一看好像没有什么问题,运行有时也能成功,但是却隐含了一个很大的bug。
通过调试:变量i的地址:0x0076e384 {1},变量mvar和mvary的地址:0x003bfe40 {1},可以看出虽然myprint函数形参i是添加了引用,但是子线程中变量i的地址和主线程中的地址不一样,所以子线程的第一个参数mvar是安全的,因为即使主线程结束mvar的内存被释放, 子线程中的遍历i依然存在。
再看子线程的第二个参数,通过指针传递,mybuf的首地址:0x003bfe18,指针pmybuf的地址:0x003bfe18,可以看出主线程中的mybuf和子线程中pmybuf变量使用的是同一块内存,所以当主线程执行完后有可能已经释放了这块地址,导致子线程引用非法内存。
所以根据以上调试得出一个结论:线程传参使用detach()函数一定不能使用指针!!!
#include <iostream>
#include <thread>
using namespace std;
void myprint(const &i ,char *pmybuf)
{
cout << i <<endl; //分析得出,mvar并不是引用的值,而是拷贝了的,所以用detach()还是可以的
cout << pmybuf <<endl;//指针绝多是有问题的,不能传递指针。
return ; //把上述的代码改成const string&pmybuf,看似是正确的
//有风险:可能是char*还没来得及转成string,就被释放了
}
int main()
{
//传递临时对象做线程的参数
//要避免的陷阱
int mvar = 1;
int &mvary = mvar;
char mybuf[] = "this is a test";
thread mytobj(myprint , mvar , mybuf);//这里的指针传递,主线程一释放就出问题。
mytobj.detach(); //所以就要const string& pmybuf 临时string
cout <<"I Love China"<<endl;
return 0;
}
陷阱二:
但是要修改上述代码存在的问题,是不是保证子线程和主线程中的参数内存地址不一样是不是就可以了呢???先看看修改成如下代码
现在已经避免了陷阱一中子线程和主线程变量内存地址一样的情况,但是现在依然还存在一个问题:mybuf到底是什么时候隐式转换成string的临时对象的呢?如果主线程已经执行完成后再转,mybuf内存先被释放,子线程就会出现无法预料的问题。
事实是,如果直接传入参数而不先隐式转换成临时对象,有可能发生内存在执行子线程前被释放的情况,依然是不安全的。
进一步修改,在16行使用string函数先隐式转换成临时对象:
这样就不会有bug了,这样就得出了一个结论:只要用临时构造的对象传递给子线程,那么一定能在主线程执行完毕前把子线程的参数构造出来
#include <iostream>
#include <thread>
using namespace std;
void myprint(const &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);//什么时候转成string的
thread mytobj(myprint , mvar ,string(mybuf));
mytobj.detach(); //事实上是:char*还没来得及转成string,就被释放了
cout <<"I Love China"<<endl; //改进:直接转换成string类型对象。
//然后帮定到了pmybuf上。
return 0;
}
再来看一个类似的案例:
第24行代码,如果直接写成:
thread mytobj(myprint,mvar, mvar);
就有可能出现主线程结束后先释放了mvar,导致子线程引用非法内存的情况。但是如果构造临时变量,就是安全的。输出如下:
需要注意的是,这时候调用了类的构造函数和拷贝构造函数,如果第15行代码不使用引用:
void myprint(const int i, const A mybuf)
这将调用两个拷贝构造函数(如下图),造成浪费,所以使用类作为传递参数时,尽量使用引用传递,节约资源。
总结:
1. 子线程参数通过构造临时对象的方式来进行传递,构造临时对象主要有两种情况
(1)传递int这种简单类型参数,建议使用值传递,不要使用引用,防止节外生枝。
(2)如果传递类对象,一律在创建线程的地方就通过隐式转换构建临时对象,然后在函数形参处使用引用(节约资源)。
2. 建议不使用detach(),只使用join(),这样就不存在局部变量失效导致线程对内存非法引用的问题
#include <iostream>
#include <thread>
using namespace std;
class A
{
public:
int m_i;
//类型转换构造函数,将一个整型转换成一个类A对象。
A(int a):m_i(a)
{
cout <<"构造函数被调用了"<<this <<endl;
}
A(const A &a):m_i(a.m_i)
{
cout <<"拷贝构造函数" <<this <<endl;
}
~A()
{
cout <<"析构函数"<<this <<endl;
}
};
void myprint(const int i ,const A &pmybuf)
{
cout << &pmybuf <<endl;
}
int main()
{
int mvar = 1;
int mysecond = 2;
thread mytobj(myprint ,mvar ,A(mysecond));
//mytobj.join();//这个是正确的 改为detach后
//mytobj.detach();//这里又出错了。mysecond还没来得及,A的构造函数还没执行。
//改为临时对象之后就又可以了
cout << "END"<<endl;
return 0;
}
总结:
1.detach()这种,thread里传递int这种简单类型参数,建议都是值传递,不要引用。
2.如果传递类对象,避免隐式类型转换,全部都在创建线程这一行就构建出临时对象来
然后在函数参数里,用引用来接,否则还会多构造(不同的编译器不同)
3.建议不使用detach(),使用join();