此篇一共讲了四个大问题:
一.传递临时对象作为线程参数
总结:
a)若传递int这种简单的类型,建议都是值传递,不建议用引用,防止节外生枝
b)如果传递类对象,避免隐式类型转换,全部都在创建线程这一行构造出临时对象,然后在函数参数里用引用进行接收,不使用值接收,否在系统还会多构造出一个对象(一共三个对象,浪费)
终极结论:
c)建议不使用detach(),使用join(),这样就不存在局部变量失效导致线程对内存的非法引用问题
#include "stdafx.h"
#include <iostream>
#include <thread>
using namespace std;
void myPrint(const int& i,char* pBuf)
{
cout << "子线程中引用变量i的地址:" << &i << endl; //引用变量i的地址
cout << "子线程中指针变量pBuf的地址:" << (void*)pBuf << endl; //字符指针pBuf指向的内存地址
cout << i << endl; //i不是val的引用,实际为值传递
cout << pBuf << endl;
return;
}
int main()
{
int val = 1; //整形变量val
int& val_ = val; //对val的引用val_
char buf[] = "this is a test"; //字符数组
cout << "主线程中变量val的内存地址:" << &val << endl; //变量val的地址
cout << "主线程中引用变量val_的内存地址:" << &val_ << endl;//引用变量val_的地址
cout << "主线程中字符数组buf的地址:" << &buf << endl; //字符数组地址即为字符数组第一个元素的地址
thread myThread(myPrint,val_,buf); //创建线程,参数:第一个为调用对象(myPrint函数),后面两个为该函数的参数
myThread.join(); //阻塞主线程,等待子线程执行完毕
//myThread.detach(); //主线程与子线程分离,两个分别执行
cout << "主线程执行..." << endl;
return 0;
}
//要避免的陷阱(存在于detach()情况下,但下面测试使用join(),保证主线程和子线程完整执行,既可以观察到完整输出结果)**
//(1)疑问:使用detach()时,如果主线程先执行完毕,变量val内存回收,则myPrint中的参数i(引用类型)是否可以获取到正确的值?
//解决:分别查看实参val_的内存地址和i的地址,如果相同,则主线程执行完后,val内存回收,则i也不会获取到正确的值
// 如果两者地址不同,说明子线程中该变量是一份拷贝,主线程执行完后,子线程可以正常执行
//test:测试结果如下图所示,子线程中引用变量i的地址与主线程中val的内存地址不同,说明创建线程时,其内部做了复制,不是真引用
//故此种情况下,子线程的引用变量i可以获取到正确的值,虽然可以正确执行,但是不建议这样做。
//(2)疑问:对于指针变量pBuf,会是怎样的情况呢?
//解决:用同样的办法对比内存地址
//test:测试结果如下图所示,buf地址和pBuf地址相同,说明此种情况下,子线程的指针变量pBuf不能获取到正确的值
//故此种情况下,子线程不能正确执行,说明detach()子线程时,绝对不可以传指针

//(3)疑问:那么问题来了,怎么可以将字符串安全的传递到子线程当中呢?????
//有一种方法:可以将函数myPrint的第二个参数改为string& pBuf,更改后的myPrint函数和执行结果如下:
void myPrint(const int i,const string& pBuf)
{
cout << "子线程中引用变量i的地址:" << &i << endl; //引用变量i的地址
cout << "子线程中pBuf的地址:" << &pBuf << endl; //字符指针pBuf指向的内存地址
cout << i << endl; //i不是val的引用,实际为值传递
cout << pBuf.c_str() << endl;
return;
}
从下图中的执行结果可以看出来,两者的地址并不相同,那么这种办法是不是真的安全呢???

//(4)疑问:是什么时候将字符数组隐式的转化为string呢,事实上存在buf已经回收(main函数执行完毕),系统才开始转换buf为string这种可能性,也就是说还是存在bug
//解决办法为:直接将buf转换为string临时对象,保证在线程中肯定有效
//修改后main函数如下:
int main()
{
int val = 1; //整形变量val
int& val_ = val; //对val的引用val_
char buf[] = "this is a test"; //字符数组
cout << "主线程中变量val的内存地址:" << &val << endl; //变量val的地址
cout << "主线程中引用变量val_的内存地址:" << &val_ << endl;//引用变量val_的地址
cout << "主线程中字符数组buf的地址:" << &buf << endl; //字符数组地址即为字符数组第一个元素的地址
thread myThread(myPrint, val_, string(buf)); //创建线程,参数:第一个为调用对象(myPrint函数)
//后面两个为该函数的参数,将buf直接转换为string类型,创建string临时对象
myThread.join(); //阻塞主线程,等待子线程执行完毕
//myThread.detach(); //主线程与子线程分离,两个分别执行
cout << "主线程执行..." << endl;
return 0;
}
//但这样就没问题了吗???万一还没有将buf转为string对象,main函数就执行完毕了呢
//下面对此问题进行测试:
****** 未使用临时构造对象 ***********
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& pBuf)
{
cout << &pBuf << endl; //pBuf对象的地址
return;
}
int main(int argc, char** argv)
{
int val = 1;
int val_A = 12;
thread myThread(myPrint, val, val_A); //val_A可以隐式转换为自定义的A类型,传递给myPrint的第二个参数
myThread.detach(); //主线程与子线程分离
return 0;
}
下面是为对子线程detach()后,发现子线程还未执行完,主线程已经退出,可以推出:主线程的变量val_A已经回收,无法隐式转换为A类型,会出现此bug(未定义行为)

****** 使用临时构造对象 ***********
//修改main函数如下:
int main(int argc, char** argv)
{
int val = 1;
int val_A = 12;
thread myThread(myPrint, val, A(val_A));//为第二个参数构造临时对象,确保在main执行完成之前,将myPrint需要的参数构造好
//在创建线程的同时构造临时对象的方法传递参数是可行的
myThread.detach(); //主线程与子线程分离
return 0;
}
可以看到输出中有有参构造、拷贝构造以及对应的析构,分析:在构建临时对象时调用了有参构造,而将此临时对象传递给函数时,thread内部并不是真正处理为引用(myPrint的第二个参数为引用类型),与第一个提出的问题一样,而是将此临时对象进行copy传递给myPrint**。
结论:只要用临时构造的A类对象作为参数传递给线程,那么就一定能够在主线程执行完之前,把线程函数的第二个参数构造出来,确保即使detach()后,子线程也能安全运行

//又有个问题,在myPrint中 为什么不使用值传递接收临时对象呢???
// 测试一下
//修改myPrint函数如下:
void myPrint(const int i, const A pBuf) //使用值传递接收临时对象
{
cout << &pBuf << endl; //pBuf对象的地址
return;
}
执行结果如下图所示,会发现多了一次拷贝构造,造成资源浪费,故传递对象时,不建议使用值传递

二.临时对象作为线程参数继续
2.1线程ID:每个线程(主线程和子线程)都对应一个ID(数字),不同的线程对应的ID自然也不同,线程ID可以用std::this_thread::get_id()来获取;
2.2临时对象构造时机捕获
class A
{
public:
int m_i;
A(int a) :m_i(a)
{
cout << "A的有参构造:" << endl;
cout << "thread ID=" << std::this_thread::get_id() << endl; //获取线程ID
}
A(const A& a) :m_i(a.m_i)
{
cout << "A的拷贝构造:" << endl;
cout << "thread ID=" << std::this_thread::get_id() << endl; //获取线程ID
}
~A()
{
cout << "A的析构" << endl;
cout << "thread ID=" << std::this_thread::get_id() << endl; //获取线程ID
}
};
void myPrint(const A& pBuf) //引用传递
{
cout << "子线程myPrint:" << endl;
cout << "thread ID=" << std::this_thread::get_id() << endl;
}
int main(int argc, char** argv)
{
int val_A = 1;
cout << "主线程ID=" << std::this_thread::get_id() << endl;
thread myThread(myPrint, val_A); //隐式类型转换
//thread myThread(myPrint, A(val_A)); //构造临时对象
myThread.detach();
//myThread.join();
return 0;
}
//根据输出结果发现,发生隐式类型转换时是在子线程中构造的A类对象,这就是问题的本质(如果主线程执行完毕,val_A被回收,则无法在子线程中构造A类对象)

//再一次测试在创建线程的时候构造临时对象
修改main函数为:
int main(int argc, char** argv)
{
int val_A = 1;
cout << "主线程ID=" << std::this_thread::get_id() << endl;
//thread myThread(myPrint, val_A);
thread myThread(myPrint, A(val_A));
//myThread.detach();
myThread.join();
return 0;
}
根据下面的执行结果可以得到:构造(有参构造和拷贝构造)都是在主线程main中执行的,故可以解决上述问题,即main函数结束之前一定会构造临时对象

//另一个测试为,当线程函数传递类对象时,不传引用,而传值时,是什么结果,
修改myPrint函数如下:
void myPrint(const A pBuf) //值传递
{
cout << "子线程myPrint:" << endl;
cout << "thread ID=" << std::this_thread::get_id() << endl;
}
根据下面的执行结果发现,值传递会进行三次构造,两次在主线程中,一次在子线程中,不建议这样使用。

三:传递类对象、智能指针作为线程参数
示例代码如下
class A
{
public:
mutable int m_i; //m_i可以修改
A(int a) :m_i(a)
{
cout << "A的有参构造:" << endl;
cout << "thread ID=" << std::this_thread::get_id() << endl; //获取线程ID
}
A(const A& a) :m_i(a.m_i)
{
cout << "A的拷贝构造:" << endl;
cout << "thread ID=" << std::this_thread::get_id() << endl; //获取线程ID
}
~A()
{
cout << "A的析构" << endl;
cout << "thread ID=" << std::this_thread::get_id() << endl; //获取线程ID
}
};
void myPrint(const A& pBuf) //使用const防止编译器报错(不使用的话老版本的编译器可能会报错,新版本编译器可能会报错)
{
pBuf.m_i = 199; //修改该值不会影响到main函数
cout << "子线程myPrint:" << endl;
cout << "thread ID=" << std::this_thread::get_id() << endl;
}
int main(int argc, char** argv)
{
A obj(10); //生成一个类对象
thread myThread(myPrint, obj);//将类对象obj作为线程参数
myThread.join();
cout << obj.m_i << endl; //仍然为10
return 0;
}
从下面的执行结果可以看出来,虽然我们在myPrint函数中传递的是引用,但是根据上面测试的种种结果来看,此处不是真正的引用,而是值传递,故在子线程中修改对象的属性值,并不会影响到主线程中传入的对象的值。

//问题:怎么让其成为真正的引用呢?即在子线程中修改对象的属性值后,主线程也随之修改
//解决:std::ref 函数
//测试:修改后的代码如下
class A
{
public:
int m_i; //去除mutable
A(int a) :m_i(a)
{
cout << "A的有参构造:" << endl;
cout << "thread ID=" << std::this_thread::get_id() << endl; //获取线程ID
}
A(const A& a) :m_i(a.m_i)
{
cout << "A的拷贝构造:" << endl;
cout << "thread ID=" << std::this_thread::get_id() << endl; //获取线程ID
}
~A()
{
cout << "A的析构" << endl;
cout << "thread ID=" << std::this_thread::get_id() << endl; //获取线程ID
}
};
void myPrint(const A& pBuf) //使用const防止编译器报错(不使用的话老版本的编译器可能会报错,新版本编译器可能会报错)
{
pBuf.m_i = 199; //修改值
cout << "子线程myPrint:" << endl;
cout << "thread ID=" << std::this_thread::get_id() << endl;
}
int main(int argc, char** argv)
{
A obj(10); //生成一个类对象
thread myThread(myPrint, std::ref(obj));//将类对象obj作为线程参数,并使用std::ref函数传入真正的引用
myThread.join();
cout << obj.m_i << endl; //该值为在子线程中修改后的值
return 0;
}
根据如下运行结果可以看出,传递到子线程中的引用变量地址与主线程中的对象地址相同,且子线程中的对象的属性值修改后,主线程中的也随之改变,故实现了真正意义上的引用传递。

智能指针作为线程参数
void myPrint(unique_ptr<int> pzn)
//使用const防止编译器报错(不使用的话老版本的编译器可能会报错,新版本编译器可能会报错)
{
cout << "myPrint线程" << endl;
cout << &pzn << endl;
}
int main(int argc, char** argv)
{
unique_ptr<int> myPtr(new int(100)); //独占式指针
thread myThread(myPrint, std::move(myPtr));
//使用std::move将myPtr指向的内容转到myPrint的参数pzn中,则myPtr就为空了
//如果不使用std::move,会出错,因为独占式指针不允许其他指针与自身同时指向同一块内存
myThread.join();
//myThread.detach();// 不可以使用detach,因为主线程如果先执行完,
//则myPtr指向的内存被回收,而子线程中pzn也指向这块被回收的内存,这样是有问题的
return 0;
}
自行测试如果不使用std::move,编译器报错情况。并使用编译器Debug功能测试子线程中智能指针与myPtr是否指向的是同一块内存
四:用成员函数指针作为线程函数
class A
{
public:
int m_i; //m_i可以修改
A(int a) :m_i(a)
{
cout << "A的有参构造:" << endl;
cout << "thread ID=" << std::this_thread::get_id() << endl; //获取线程ID
}
A(const A& a) :m_i(a.m_i)
{
cout << "A的拷贝构造:" << endl;
cout << "thread ID=" << std::this_thread::get_id() << endl; //获取线程ID
}
~A()
{
cout << "A的析构" << endl;
cout << "thread ID=" << std::this_thread::get_id() << endl; //获取线程ID
}
void thread_work(int num) //成员函数做线程函数
{
cout << "子线程thread_work" << endl;
}
void operator()(int num) //重载()操作符,作为线程函数
{
cout << "子线程()" << endl;
}
};
int main(int argc, char** argv)
{
A obj(10); //生成一个A类对象
thread myThread1(&A::thread_work, obj, 15); //创建线程1(使用成员函数thread_work),此种情况下用detach也可以,子线程中为该对象obj的拷贝,会调用拷贝构造
//thread myThread2(&A::thread_work, std::ref(obj), 15); //此处测试过程中出错,不能使用std::ref
thread myThread3(&A::thread_work, &obj, 15); //创建线程3(使用成员函数thread_work),也是真正的引用,不能使用detach, &obj等价于std::ref(obj),不调用拷贝构造
thread myThread4(obj,15); //创建线程4(使用A类中的重载()的成员函数)
thread myThread5(std::ref(obj), 15); //创建线程5(使用A类中的重载()的成员函数),真正的引用,不能使用detach
//thread myThread6(&obj, 15); //此处测试过程中出错,不能使用&
//有些可以使用std::ref,而有些不能,有些可以使用&,而有些不能,可以自己尝试
myThread1.join();
//myThread2.join();
myThread3.join();
myThread4.join();
myThread5.join();
//myThread6.join();
return 0;
}
具体结果可以自己运行测试。