|
传递临时对象作为线程参数
先看下面的范例
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!";
std::thread mytobj(myprint, mvar, mybuf);
mytobj.join();
cout << "main主函数执行结束!" << endl;
return 0;
}
执行结果

注意:虽然myprint的第一个参数i是引用类型,但是形参i的地址和原来main主函数中mvar地址不同。这是因为thread类的构造函数实际上是复制了mvar到线程中,与thread的实现方式有关。这种做法是线程安全的,后面会解释为什么这是线程安全。?

detach的潜在危险
???????上面的代码范例中,子线程中的pmybuf的值就是主线程中mybuf的值,也就是说两者指向同一块内存。detach会使主线程和子线程分离,主线程可能先于子线程运行结束,那pmybuf就可能引用到主线程中已经回收的内存空间,其结果是未知的。
若使用detach方式创建线程,不要往线程中传递引用、指针之类的参数。
那如何将字符串作为参数传递到线程中呢?
void myprint(const int& i, const string& pmybuf)
{
cout << i << endl;
cout << pmybuf << endl;
return;
}
????????若是修改myprint的第二个参数为对string类型的引用。c++中只会为const引用产生临时对象,因此虽然是使用了引用&string,但实际上还是发生了对象复制(复制出来的对象是在thread内部构造出来的),这个与系统内部工作机制有关。
? ? ? ? 上述做法仍然存在风险,即发生对象复制的时机可能在主线程运行结束之后。所以这种做法仍然存在风险。
正确做法:原地构造临时对象。
std::thread mytobj(myprint, mvar, string(mybuf));
为什么构造临时对象就没问题了呢?
写一个类来验证结论。
class A
{
public:
A(int a) :m_i(a) {
cout << "A::A(int a)构造函数执行" << this << endl;
}
A(const A& a) {
cout << "A::A(const A)拷贝构造函数执行" << this << endl;
}
~A()
{
cout << "~A::A()析构函数执行" << this << endl;
}
int m_i;
}
同时修改myprint函数,打印pmybuf对象的地址
void myprint(int i, const A& pmybuf)
{
cout << &pmybuf << endl;
return;
}
示例代码如下
class A
{
public:
A(int a) :m_i(a) {
cout << "A::A(int a)构造函数执行" << this << endl;
}
A(const A& a) {
cout << "A::A(const A)拷贝构造函数执行" << this << endl;
}
~A()
{
cout << "~A::A()析构函数执行" << this << endl;
}
int m_i;
};
void myprint(int i, const A& pmybuf)
{
cout << &pmybuf << endl;
return;
}
int main ()
{
int mvar = 1;
int mysecondpar = 12;
std::thread mytobj(myprint, mvar, mysecondpar);//希望mysecondpar转成A类型对象传递
//给myprint的第二个参数
//std::thread mytobj(myprint, mvar, A(mysecondpar));
mytobj.join();
//mytobj.detach();
cout << "main主函数执行结束!" << endl;
return 0;
}

第一行说明通过mysecondpar构造了一个A类对象,作为myprint的第二个参数。两者打印出的this值相同,说明是myprint的第二个参数对象确实是由mysecondpar构造出来的。
此时将上面的join()换成detach()。
?
?本意是通过mysecondpar构造一个A类对象,然后作为参数传递给myprint线程入口函数。但是从结果来看,A类对象还没有构造出来,主线程就运行结束了。若用主线程中已经销毁的对象来构造A类对象,将导致未定义的结果。
修改创建线程的代码
std::thread mytobj(myprint, mvar, A(mysecondpar));

?因为detach的原因,多次运行可能结果有差异。但都是只执行一次构造函数、一次拷贝构造函数,而且线程myprint中打印的对象(pmybuf)的地址应该就是拷贝构造函数所创建的对象的地址。(detach使主线程和子线程分离,输出窗口关联的是主窗口,因此这里看不到myprint的输出)。
??????? 虽然myprint的第二个参数是引用类型,但是thread还是用thread的构造函数中拷贝了一个对象出来(因此调用了A类的拷贝构造函数)。这是thread内部的处理方式。
因此,用临时对象的方式,能使对象A在主线程运行结束之前就构造出来。
阶段小结
1、传递int这种简单数据类型,建议使用值传递,不要使用引用类型。
2、如果传递类对象作为参数,则避免隐式类型转换(例如把一个char*转成string,把一个int转成类A对象),全部都在创建线程这一行就构造出临时对象来,然后线程入口函数的形参位置使用引用来做形参(如果不使用引用可能在某种情况下导致多构造一次临时对象,不但浪费还会造成新的潜在危险。这样做的目的无非就是想办法避免主线程退出导致子线程对内存的非法引用。
3、建议不使用detach,只使用join,这样就不存在局部变量失效导致线程对内存的非法引用的问题。
临时对象构造函数时机
线程id:线程id是唯一标识线程的数字,可以用c++标准库里的函数std::this_thread::get_id来获取。
class A
{
public:
A(int a) :m_i(a) {
cout << "A::A(int a)构造函数执行,this = " << this << ",threadid = " << std::this_thread::get_id() << endl;
}
A(const A& a) {
cout << "A::A(const A)拷贝构造函数执行,this = " << this << ",threadid = " << std::this_thread::get_id() << endl;
}
~A()
{
cout << "~A::A()析构函数执行,this = " << this << ",threadid = " << std::this_thread::get_id() << endl;
}
int m_i;
};
void myprint2(const A& pmybuf)
{
cout << "子线程myprint2的参数pmybuf的地址是:" << &pmybuf << ",threadid = " << std::this_thread::get_id() << endl;
}
int main() {
cout << "主线程id = " << std::this_thread::get_id() << endl;
int mvar = 1;
std::thread mytobj(myprint2, mvar);
//std::thread mytobj(myprint2, A(mvar));
mytobj.join(); //用join方便观察
cout << "main主函数执行结束!" << endl;
}

????????通过上面的结果可以发现,mvar通过类A的类型转换构造函数生成myprint2需要的pmybuf对象,根据线程id可以发现,生成对象的时机是在子线程中。如果使用detach就可能存在主线程提前执行完毕的风险。
修改这一行代码
std::thread mytobj(myprint2, A(mvar));

?会发现:线程入口函数主要的形参是在主线程中就构造完毕了,执行了一次拷贝构造函数(而不是在子线程中才构造)。这说明即时主线程运行结束,myprint2需要的形参也已经构造完毕了。这保证了线程安全。
若是把myprint2的形参修改为非引用呢?
void myprint2(const A pmybuf){...}

?可以看到执行了两次拷贝构造函数,一次是在主线程中,一次是在子线程中。这样子线程就有可能引用主线程中已经失效的内存。所以线程入口函数myprint2的类类型形参应该使用引用。
用成员函数指针作为线程入口函数
类A中增加了thread_work成员函数,重载了operator(),代码修改如下
#include<thread>
#include<iostream>
using namespace std;
class A
{
public:
A(int a) :m_i(a) {
cout << "A::A(int a)构造函数执行,this = " << this << ",threadid = " << std::this_thread::get_id() << endl;
}
A(const A& a) {
cout << "A::A(const A)拷贝构造函数执行,this = " << this << ",threadid = " << std::this_thread::get_id() << endl;
}
~A()
{
cout << "~A::A()析构函数执行,this = " << this << ",threadid = " << std::this_thread::get_id() << endl;
}
public:
void thread_work(int num) //带一个参数
{
cout << "子线程thread_work执行,this = " << this << ",threadid = " << std::this_thread::get_id() << endl;
}
int m_i;
};
int main() {
A myobj(10);
std::thread mytobj(&A::thread_work, myobj, 15);
mytobj.join();
cout << "main主函数执行结束!" << endl;
return 0;
}

?从上面的结果(this值)可以看出,类A的拷贝构造函数是在主线程中执行的(说明是复制了一个类A的对象),而析构函数是在子线程中执行的。
??????? main函数中创建thread对象时的第二个参数也可以是一个对象地址,也可以是std::ref。
修改代码:
std::thread mytobj(&A::thread_work, &myobj, 15);//第二个参数也可以是std::ref(myobj)

可以发现 ,没有调用类A的拷贝构造函数新建一个对象,直接使用了主线程中的对象。因此必须用mytobj.join()。如果使用detach将不安全。
传递类对象作为线程参数
由于调用了拷贝构造函数,因此传递给线程入口函数的对象实际上是实参对象的复制(虽然该复制是在子线程中构造出来的)。这意味着即使改变了线程入口函数中对象的内存也不会影响到实参。
修改类A中的成员变量为mutable(因为下面将类A用const修饰,但又想改变成员变量)
class A
{
public:
A(int a) :m_i(a) {
cout << "A::A(int a)构造函数执行,this = " << this << ",threadid = " << std::this_thread::get_id() << endl;
}
A(const A& a) {
cout << "A::A(const A)拷贝构造函数执行,this = " << this << ",threadid = " << std::this_thread::get_id() << endl;
}
~A()
{
cout << "~A::A()析构函数执行,this = " << this << ",threadid = " << std::this_thread::get_id() << endl;
}
mutable int m_i;
};
修改线程入口函数
void myprint2(const A& pmybuf)
{
pmybuf.m_i = 199; //修改该值不会影响到main主函数中实参的该成员变量
cout << "子线程myprint2的参数pmybuf的地址是:" << &pmybuf << ",threadid = " << std::this_thread::get_id() << endl;
}
完整代码如下
#include<thread>
#include<iostream>
using namespace std;
class A
{
public:
A(int a) :m_i(a) {
cout << "A::A(int a)构造函数执行,this = " << this << ",threadid = " << std::this_thread::get_id() << endl;
}
A(const A& a) {
cout << "A::A(const A)拷贝构造函数执行,this = " << this << ",threadid = " << std::this_thread::get_id() << endl;
}
~A()
{
cout << "~A::A()析构函数执行,this = " << this << ",threadid = " << std::this_thread::get_id() << endl;
}
mutable int m_i;
};
void myprint2(const A& pmybuf)
{
pmybuf.m_i = 199; //修改该值不会影响到main主函数中实参的该成员变量
cout << "子线程myprint2的参数pmybuf的地址是:" << &pmybuf << ",threadid = " << std::this_thread::get_id() << endl;
}
int main() {
A myobj(10); //生成一个类对象
std::thread mytobj(myprint2, myobj); //将类对象作为线程参数
//std::thread mytobj(myprint2, std::ref(myobj));
mytobj.join();
//mytobj.detach();
cout << "m_i: "<<myobj.m_i<<endl;
cout << "main主函数执行结束!" << endl;
return 0;
}

?可以看出m_i并没有被修改为199。
涉及到的语法规则是:c++语言只会为const引用产生临时对象,否则编译器可能会报错。
myprint2线程入口函数的形参涉及产生临时对象,所以必须加const。

?但是这产生了另外一个问题:如果加了const修饰,那么修改pmybuf对象中的数据成员会变得非常不便,需要修改的成员需使用mutable修饰。还有一个问题:虽然myprint函数的形参是一个引用,但是修改并不会影响到实参,那么如果真的需要修改这个形参呢?答案是使用std::ref,这是一个函数模板。

?修改创建thread类型对象的行
std::thread mytobj(myprint2, std::ref(myobj))
?那么myprint2的形参中可以去掉const修饰
void myprint2(A& pmybuf) {...}

可以看到m_i被成功修改。
?
?
|