01C++11多线程编程之thread,join,detach,joinable以及简说detach传引用地址的大坑
1 thread类对象创建线程与join回收线程
-
1)thread创建线程很简单,定义一个对象然后传一个可调用对象即可。
可调用对象:普通的回调函数,类内重载括号()运算符传类对象,lambda表达式,packed_task模板类打包任务等等都可以。
-
2)线程回收join的原因:主要目的是回收进程控制块PCB,保存了一些线程函数返回值等内容,若不回收可能top查看VIRT虚拟内存和RES进程所占内存比较大,因为一般开启一个线程系统会开辟一个64M的堆栈空间。
代码:
#include<iostream>
#include<thread>
using namespace std;
void myprint(){
cout << "线程开始执行了!" << endl;
cout << "线程执行结束了!" << endl;
}
int main(){
//1 创建线程,myprint为可调用对象。
thread myobj(myprint);
//2 join函数的作用:阻塞在调用者线程,等待回收子线程资源。
myobj.join();
cout << "主线程执行完成!" << endl;
return 0;
}
![在这里插入图片描述](https://img-blog.csdnimg.cn/2020122622482877.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDUxNzY1Ng==,size_16,color_FFFFFF,t_70)
2 detach分离线程
detach的作用是使线程分离,分离后当主线程结束后交由系统管理,具体可去看我僵尸进程的讲述。当创建thread对象后,要么调用join要么调用detach,不能同时调用(我试的时候编译没错,执行完后报错)。
不建议使用detach,当然必须要用的话也可以,注意一下即可。
1)看下列使用detach例子代码。
#include<iostream>
#include<thread>
using namespace std;
void myprint(){
cout << "1!" << endl;
cout << "2!" << endl;
cout << "3!" << endl;
cout << "4!" << endl;
cout << "5!" << endl;
cout << "6!" << endl;
}
int main(){
//1 创建线程,myprint为可调用对象。
thread myobj(myprint);
//2 join函数的作用:阻塞在调用者线程,等待回收子线程资源。
//myobj.join();
//3 detach,线程分离
myobj.detach();
cout << "主线程执行完成!" << endl;
return 0;
}
上面代码执行结果分析:下面看到,子线程中打印了6条数字,但屏幕却显示了3条,这是因为主线程结束了,子线程的宿主线程死亡,系统会将该子线程挂载到一个新的主线程,方便回收防止产生僵尸进程。
这样的话,由于此时子线程与已经结束的主线程的屏幕已经毫无关系,所以子线程剩余的打印内容并不会输出到屏幕上。所以detach之后一般称为后台任务,注意,虽然没有打印完整,但是程序还是正常执行完毕的,你的代码逻辑还是会正常工作,所以不必担心,只是让你知道这种情况而已。
如果你想让子线程的内容完整打印到屏幕,可以让主线程不结束即可,例如睡眠5s。
![在这里插入图片描述](https://img-blog.csdnimg.cn/20201226225810446.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDUxNzY1Ng==,size_16,color_FFFFFF,t_70)
3 joinable判断是否可以join或者detach
joinable的作用:返回true:代表可以join或者detach;否则说明已经join或者detach,不能再调用。
#include<iostream>
#include<thread>
using namespace std;
void myprint(){
cout << "1!" << endl;
cout << "2!" << endl;
cout << "3!" << endl;
cout << "4!" << endl;
cout << "5!" << endl;
cout << "6!" << endl;
}
int main(){
//1 创建线程,myprint为可调用对象。
thread myobj(myprint);
//2 join函数的作用:阻塞在调用者线程,等待回收子线程资源。
//myobj.join();
if (myobj.joinable()) {
myobj.join();
//myobj.detach();
cout << "可以join或者detach" << endl;
}
else {
cout << "不能再join或者detach" << endl;
}
cout << "主线程执行完成!" << endl;
return 0;
}
结果正确。
![在这里插入图片描述](https://img-blog.csdnimg.cn/20201226231214455.png)
4 detach传参为可调用对象,并且参数存在为引用或者地址的问题(detach传引用地址的大坑)
这里先简单介绍当detach的线程函数传可调用对象时容易出现的错误。记住,第四点的问题皆因调用detach才会引起的问题。也是本节最重要的一点。
一般可调用对象传引用的方式为:
普通回调:函数的形参为引用或者指针。(实际上detach时形参为引用也会拷贝,这个详细看下一章)
类内重载括号:类对象的内部数据成员声明为引用,然后子线程通过回调指针指向该对象地址的方式访问(这是类对象重载括号能作回调函数的根本原因),但是不建议使类内的数据成员声明为引用。下面的代码例子是使用这种方法讲解而已。
lambda表达式:与普通回调一样。
1)类对象重载括号作可调用对象传参,并且类对象成员存在引用(一般声明类很少类成员声明为引用)。
首先先抛出两个问题:
1)当主线程结束了,detach的子线程在括号()重载访问m_i会怎么样?
2)当主线程结束了,detach的子线程能再访问到ta对象吗,ta对象不是被释放了吗?
#include<iostream>
#include<thread>
using namespace std;
//类中包含operator(),此类的对象为可调用对象
class Ta{
public:
Ta(int &i) : m_i(i){
cout << "Ta()构造函数被执行了!" << endl;
};
Ta(const Ta &ta) : m_i(ta.m_i){
cout << "Ta()复制构造函数被执行了!" << endl;
}
~Ta(){
cout << "~Ta()析构函数被执行了!" << endl;
}
void operator()(){
//打印多条是为了让主线程先执行完毕,让m_i引用消失
cout << "我的线程operator()开始执行了!" << endl;
cout << "我的线程operator()执行结束了!" << endl;
cout << "我的线程operator()执行结束了!" << endl;
cout << "我的线程operator()执行结束了!" << endl;
cout << "我的线程operator()执行结束了!" << endl;
cout << "我的线程operator()执行结束了!" << endl;
cout << "我的线程operator()执行结束了!" << endl;
cout << "我的线程operator()执行结束了!" << endl;
cout << "我的线程operator()执行结束了!" << endl;
cout << "m_i的值为:" << m_i << endl;
}
private:
int &m_i;
};
int main()
{
int myi = 6;
Ta ta(myi);
thread myobj(ta);//ta为可调用对象
//myobj.join(); //等待子线程执行结束
//此时,如果主线程执行完了,myi就被释放了;子线程还没执行完,就会出现不可预料的bug;
myobj.detach();
cout << "主线程执行完毕!" << endl;
return 0;
}
看结果分析:
![在这里插入图片描述](https://img-blog.csdnimg.cn/20201227171723808.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDUxNzY1Ng==,size_16,color_FFFFFF,t_70)
-
1)先解释第一个问题:看上面结果知道,当主线程先结束,detach的子线程再通过括号访问m_i引用时,会出现访问错误,原因是m_i变量引用已经被释放了,所以多次访问会出现不同的值。
-
2)第二个问题:上面其实已经说过,thread(ta)创建线程后再detach,ta在子线程为拷贝值,所以即使主线程结束了也不会出现问题。记住是detach时才会出现即使对象传引用也会拷贝值,但内部数据成员有引用时即m_i,该m_i数据是不会创建内存而直接引用,所以上面会报错,这一点很重要。总结第二点来说:detach时,即使传引用对象它也会进行拷贝,但若对象内部存在引用数据成员,该内部数据成员不会拷贝。
例如:
//整个对象除了m_i不拷贝,其余内容都会拷贝
class A{
int &m_i;//不会拷贝
int m_j;//会拷贝
char m_k;//会拷贝
};
总结:
- 所以只要这个Ta类对象没有引用,没有指针,那么就不会产生问题。并且建议大家能使用join就使用join,使用join就不会出现上面的问题。
3)测试detach时对象会进行拷贝:
看上面的结果图,可以看到Ta()复制构造函数被执行了,所以detach就是会调用拷贝赋值。
5 可调用对象传lambda表达式
一般lambda表达式都是执行比较小的函数,所以是非常简单的,只要大概会使用即可。
#include<iostream>
#include<thread>
using namespace std;
int main(){
auto myLambda = []() {
cout << "子线程执行开始!" << endl;
cout << "子线程执行完毕!" << endl;
};
thread myobj(myLambda);
if (myobj.joinable() == true) {
myobj.join();
//myobj.detach();//不建议使用,主线程执行完毕的话子线程容易出现访问错误
}
cout << "主线程执行完毕!" << endl;
return 0;
}
上面程序结果,使用join程序输出非常稳定。
![在这里插入图片描述](https://img-blog.csdnimg.cn/20201227174155729.png)
6 总结本节
总而言之,能不用detach就别用,这样可以使我们不用考虑detach传参的大坑问题。