线程启动、结束,创建线程方法,join,detach
1、范例演示:线程的开始和结束
线程开始、结束已经保持运行的规则:
1 、程序运行起来,生成一个进程,该进程所属的主线程开始自动运行,主线程从main()函数开始,main()函数返回,则整个进程执行完毕;
2、主线程从main()函数开始执行,那么我们自己创建的线程,也需要从一个函数开始运行(初始函数),一旦这个函数运行完毕,就代表我们这个线程运行结束);
3、整个进程执行完毕的标志为:主线程执行完毕,一旦主线程执行完毕代表整个进程执行完毕;而如果其他依赖这个主线程的子线程还没有执行完毕,则会被操作系统强行终止;
4、一般情况下,如果大家想保持子线程运行状态的话,要让其依赖的主线程一直保持运行。
注意:凡事均有例外,第3、4条也有其他情况,也就是后续所讲的detach()函数。
线程的创建步骤:
1、包含thread头文件;
2、子线程开始的函数存在(自己编写);
3、main函数创建线程,一般是使用thread类创建对象即可,这时线程就已经运行了。
4、一般情况下,子线程开始就要进行如下两种情况:一是使用join()阻塞主线程,等待子线程执行结束,再继续执行主线程;二是使用detach()使得子线程与主线程分离,主线程不必等子线程,不影响子线程的执行。
示例:创建子线程,join()函数阻塞主线程
#include <iostream>
#include <thread>
using namespace std;
// 线程执行的入口函数
void myPrint()
{
cout << "我的线程开始运行" << endl;
//-------------
//-------------
cout << "我的线程运行完毕" << endl;
return;
}
int main(int argc, char* argv[])
{
// (1)使用thread类创建了一个子线程,线程执行入口函数是myPrint
// (2)创建线程的同时,子线程就开始执行了
thread myThread(myPrint);
// (3) join()阻塞主线程, 等待myPrint执行完,当myPrint执行完毕,join()就执行完毕,主线程继续往下执行
myThread.join();
cout << "Hello World!" << endl;
return 0;
}
调试步骤:
在 join() 设置断点可看到主线程等待子线程的过程
F11逐语句,就是每次执行一行语句,如果碰到函数调用,它就会进入到函数里面
F10逐过程,碰到函数时,不进入函数,把函数调用当成一条语句执行
示例:创建子线程,detach()函数分离子线程与主线程
#include <iostream>
#include <thread>
using namespace std;
void myPrint()
{
cout << "我的线程开始运行" << endl;
//-------------
//-------------
cout << "我的线程运行完毕" << endl;
return;
}
int main(int argc, char* argv[])
{
thread myThread(myPrint);
// detach():分离,主线程不再与子线程汇合,不再等待子线程
// detach()后,子线程和主线程失去关联,驻留在后台,由C++运行时库接管
// 也就是说,无论主线程存在与否,子线程都会再后台运行,直至其绑定的函数执行结束
// 为什么引入detach():我们创建了很多子线程,让主线程逐个等待子线程结束,这种编程方法不太好,所以引入了detach
myThread.detach();
cout << "Hello World!" << endl;
return 0;
}
总结:
1、使用thread类创建一个对象时,就有两个线程在跑了,相当于整个程序的执行有两条线在同时走,所以,可以同时干两个事,即使一条线被堵住了,另外一条线也是可以通行的,这就是多线程;
2、detach()函数分离了主线程和子线程,主线程和子线程不再汇合,主线程不必等子线程,不影响子线程的执行。一旦detach()之后,与主线程关联的thread对象就会失去与这个主线程的关联,此时这个子线程就会驻留在后台运行,这个子线程就相当于被C++运行时库接管,当这个子线程执行完成之后,由运行时库负责清理该线程相关的资源(守护线程)。这也导致我们对子线程myprint失去了控制权;
3、但是,失去对子线程的控制权不太好,一个书写良好的程序,应该是主线程等待子线程执行完毕后,自己才能最终退出,也就是使用join()阻塞主线程,等待子线程执行结束,再继续执行主线程。
2、joinable()
判断是否可以成功使用 join() 或者 detach(); 返回 true(可以用join()或者detach())或者 false (不能使用join()或者detach())
#include <iostream>
#include <thread>
using namespace std;
void myPrint()
{
cout << "我的线程开始运行" << endl;
//-------------
//-------------
cout << "我的线程运行完毕" << endl;
return;
}
int main(int argc, char* argv[])
{
thread myThread(myPrint);
myThread.join();
// joinable()判断是否可以成功使用join()或者detach()
// 如果返回true,证明可以调用join()或者detach()
// 如果返回false,证明调用过join()或者detach(),join()和detach()都不能再调用了
if (myThread.joinable())
{
cout << "可以调用可以调用join()或者detach()" << endl;
}
else
{
cout << "不能调用可以调用join()或者detach()" << endl;
}
cout << "Hello World!" << endl;
return 0;
}
3、其他创建线程的方式
利用可调用对象来构造子线程,C++中可调用对象包括:函数、函数指针、lambda表达式、bind创建的对象以及仿函数对象。
3.1、仿函数对象
#include <iostream>
#include <thread>
using namespace std;
//创建一个类,并编写圆括号重载函数,初始化一个该类的对象,把该对象作为线程入口地址
class Student {
public:
Student(){}
// 圆括号重载
void operator()() {
cout << "我的线程开始运行" << endl;
//-------------
//-------------
cout << "我的线程运行完毕" << endl;
}
};
int main()
{
// 利用仿函数对象构建子线程
Student stu;
thread myThread(stu);
myThread.join();
cout << "I Love China" << endl;
return 0;
}
引入一个bug:对于detach()函数,使用仿函数,在类中以指针、引用等引入主线程的局部变量,那么在主线程结束时,该变量被释放,那么子线程中使用该变量的进行处理就会出现错误。
#include <iostream>
#include <thread>
using namespace std;
class Student {
public:
Student(int& id):m_Id(id){}
void operator()() {
cout << "我的学生1" << m_Id << endl;
cout << "我的学生2" << m_Id << endl;
cout << "我的学生3" << m_Id << endl;
cout << "我的学生4" << m_Id << endl;
cout << "我的学生5" << m_Id << endl;
}
private:
int m_Id;
};
int main()
{
// 利用仿函数对象构建子线程
int a = 6;
Student stu(a);
thread myThread(stu);
myThread.detach();
cout << "I Love China" << endl;
return 0;
}
运行结果:
可以看出,使用detach()函数,使得在初始化时的参数无法无法正常使用。故而,在使用仿函数对象构建线程时,不在该类中使用引用、指针,一般不会出现问题。
那么大家可能还有一个疑问:一旦调用了detach(),那主线程执行结束了,这里用的这个stu对象还在吗?
解释:对象不在了,但是,这个对象实际上是被 复制(深拷贝) 到子线程中去;执行完主线程后,stu会被销毁,但是所复制的Student对象依旧存在。所以,只要这个Student对象里没有引用、没有指针,那么就不会产生问题。
#include <iostream>
#include <thread>
using namespace std;
class Student {
public:
Student(int id):m_Id(id){
cout << "构造函数被执行了" << endl;
}
Student(const Student& stu) :m_Id(stu.m_Id) {
cout << "深拷贝构造函数被执行了" << endl;
}
~Student() {
cout << "析构函数被执行了" << endl;
}
void operator()() {
cout << "我的学生1:" << this->m_Id << endl;
cout << "我的学生2:" << m_Id << endl;
cout << "我的学生3:" << m_Id << endl;
cout << "我的学生4:" << m_Id << endl;
cout << "我的学生5:" << m_Id << endl;
}
private:
int m_Id;
};
int main()
{
// 利用仿函数对象构建子线程
int a = 6;
Student stu(a);
thread myThread(stu);
myThread.detach();
cout << "I Love China" << endl;
return 0;
}
VScode调试结果(这里类对象传入的是整型变量):
从结果中,可以发现,在构建子线程时,调用了类中的深拷贝构造函数,那么使用detach()时,即使仿函数对象被主线程释放掉,那么子线程依旧可以正常执行。这里没有看到子线程的析构函数,可以把detach()函数换成join()即可看到更加清晰看到子线程的析构函数执行,包括传入的参数变量。如下:
注意:
1、使用使用join()函数与detach()函数时,释放的类对象实现时间视情况而定;
2、另外,在使用VS2019时,可能是编译器的原因,即使使用整型变量构建类对象stu后,没有使用指针或者引用,依然无法使得子线程中正常使用传入的参数,结果如下图。所以,后续的文章中均使用VScoode进行调试。
3.2、lambda表达式
#include <iostream>
#include <thread>
using namespace std;
int main()
{
// lambda表达式 线程的入口
auto lambdaThread = [] {
cout << "我的线程开始执行了" << endl;
//-------------
//-------------
cout << "我的线程开始执行了" << endl;
};
thread myThread(lambdaThread);
myThread.join();
cout << "I Love China" << endl;
return 0;
}