文章目录
- 为什么要有线程?
- Linux对于线程的设计
- Linux线程特点总结
- Linux线程和进程的比较
- 线程的优点
- 线程的缺点
- Linux下线程的使用
- 线程的创建与销毁
- 线程退出的三种方式
- 什么是线程ID?
- 线程局部存储
- 线程分离
- exit对于线程的影响
为什么要有线程?
假设没有线程,操作系统执行程序的基本单位就是进程,一个进程执行一个程序,进程就是系统下的执行流。现在我在用Markdown编辑器写博客,这个编辑器可能一分钟会自动保存一次,除了自动保存,Markdown的主要功能就是从键盘读取我输入的数据,如果一分钟的时间到了,运行读取键盘数据的进程就会被切换成向磁盘写入数据(自动保存)的进程了,所以每到一分钟,我的键盘就会被屏蔽一段时间,我也就无法码字了。
虽然屏蔽的时间不会太长,但是这样的体验很明显没法满足我的写作需求,然而一个进程不可能一边访问我的键盘,一边又向磁盘写入数据,因为这是两个执行流,只有进程的操作系统需要两个进程执行两个执行流,所以我的写作过程总是会出现一些需求被屏蔽的情况(除了码字的阻塞,还会有其他功能的阻塞)。
但是你又不能再创建一个进程,以共享内存的方式与当前进程进行通信,如果我对Markdown编辑器的需求不止两个呢?是不是就要创建很多进程,然后让它们通信,不需要时再关闭这些进程,但这样做的效率也太低了,我们不考虑创建进程的消耗,单是进程间通信的消耗已经很大了,而且进程间还要频繁地进行通信,不出意外的话,这样的操作系统性能是很低的。
所以我们需要一个新的结构,它可以与进程共享数据,同时还可以在进程下运行,承担进程作为执行流的工作,以解决进程阻塞影响程序使用的痛点。为了满足需要,操作系统设计师就设计了一个新的结构——线程。
Linux对于线程的设计
学习进程的时候,我们知道Linux用task_struct结构体描述进程,进程运行之前,系统需要将程序从磁盘加载到内存,然后系统创建task_struct结构体用来描述进程的相关消息,Linux下的task_struct对应于操作系统的进程控制块,task_struct含有一些很重要的结构,比如进程地址空间,维护进程地址空间与物理内存空间之间映射关系的页表等等,之前我们对进程的理解是:进程=内核数据结构+物理内存上的代码,其中的内核数据结构就是task_struct,物理内存上的代码就是从磁盘加载到内存上的那部分数据
但是对于进程的内核数据结构,不能只是简单地理解为task_struct结构体,它还含有进程地址空间,页表等等资源。当系统要创建一个新的进程时,就需要对这些资源进行分配,这个分配的过程很重要,它会创建新的task_struct结构体,重新建立进程地址空间,通过页表将物理内存上的数据与进程地址空间上的数据建立映射,单是这些工作就需要消耗操作系统大量的资源(比如时间,内存),此外还有一些别的操作,这里不再赘述,由于创建进程会消耗操作系统较多的资源,所以进程的创建需要很高的成本。
对操作系统来说,为了从软件层面提高系统的性能,进程能少创建一些就少创建一些。由于进程拥有大量资源可以使用,执行一项任务通常不会使用所有的进程资源,所以操作系统为了执行一项任务而创建一个新的进程是没有必要的,操作系统完全可以重复使用原来的进程资源,将新的任务加载到原来的进程中,只是需要满足两个任务使用的资源互相不冲突的前提条件。
之前说过父进程fork创建子进程,我们可以通过if else的条件判断使两个进程执行不同的代码,从某种角度上理解,执行不同的代码是访问不同的资源的一种形式,因为它们的函数栈帧肯定不同,使用的进程地址空间也就不同。那么对于一个进程的不同任务,我们也能通过类似的方式使它们占用不同的资源,所以我们能够做到对不同任务分配不同的资源。
因此我们可以实现在一个进程下执行不同任务的前提条件——使不同任务占用不同的资源,操作系统将进程下的任务叫做执行流,也叫做线程,为提高系统的性能,工程师需要设计实现线程,Windows系统对于线程的设计是:重新设计描述线程的结构体,重新设计线程的切换,调度算法,而线程作为处于进程内的结构,一个进程中可能含有多个线程,它们是两个不同的结构,所以线程和进程之间的关系也需要维护,这就使得进程和线程的耦合关系变得非常复杂,所以Windows对于线程的设计十分的繁琐。
而Linux对于线程的设计想法是:线程与进程具有很多的相似性,进程需要维护上下文数据,线程也需要,进程需要调度,线程也需要,进程需要切换,线程也需要,两者的区别无非就是进程的数据多一些,线程的数据少一些。所以Linux没有为了线程特地设计一套算法,而是复用进程的算法与结构。
Linux创建新的进程,就需要创建task_struct,并建立新的进程地址空间与页表,而创建新的线程,系统只需要创建新的task_strcut,不需要为其分配地址空间和页表资源,线程与其所属的进程使用同一份进程地址空间与页表,进程的资源对于线程来说是可见的。所以对于操作系统,创建线程只是创建一个结构体,不需要创建什么地址空间,页表,只创建task_struct,效率非常高。所以在Linux下,没有线程的概念,或者说线程只是由task_struct模拟的
或者可以这么理解,Linux下不仅没有线程的概念,而且也没有进程的概念,Linux只有task_struct结构体,以及执行流的概念,task_struct对应的就是执行流!以前我们说task_struct是进程的结构体,用来描述一个进程,放在现在来看,task_struct不再是进程,而是执行流
对于进程我们也有了新的认识,以前的进程只是内核数据结构task_struct与物理内存上的代码数据,现在我们就可以这样理解,除了物理内存上的代码数据以及task_struct,进程的内核数据结构还包括了进程地址空间,维护地址映射关系的页表,这些关于资源的一整套机制,我们将其称之为进程,所以从资源的角度看进程,进程是承担分配系统资源的基本单位,在进程下的线程使用着这些资源。
以前我们了解的进程,内部只有一个执行流,要执行其他任务就需要再创建一个进程,此时我们对进程的认知是单执行流进程,现在我们所知道的进程,内部可以有多个执行流,一个进程可以执行多个任务,我们称之为多执行流进程,系统调度的基本单位就是执行流,也就是位于进程下的线程,所以线程是操作系统的基本调度单位。
对于实现了线程结构的操作系统来说,它们的PCB资源大小是大于等于Linux的task_struct大小的。当Linux的进程只有一个执行流时,两者大小相等,因为task_struct就是PCB,而真线程系统的进程也是PCB。当Linux的进程有多个执行流时,Linux的PCB资源大小小于其他系统,因为作为PCB资源的task_struct是一个线程的属性,是一个线程所占用的资源,显然task_struct所占用的资源是小于一个进程的,而真系统系统的PCB对应着进程,所以此时Linux的PCB小于真线程系统的PCB。
Linux线程特点总结
操作系统的教科书对线程的特点总结为以下三点
线程是进程内部运行的执行流
线程比进程的粒度更细,调度成本更低
线程是CPU调度的基本单位
1.线程是进程内部运行的执行流:由于操作系统创建进程是为了分配一个执行流给进程执行,但是创建进程需要消耗大量的资源,一个执行流通常不会占用所有的进程资源,用进程完成执行流的执行未免有些大材小用,于是我们引入了线程的概念,线程位于进程的内部,使用进程的一部分资源,所以线程是在进程内部运行的执行流,线程只使用进程的一部分资源,用线程完成执行流的执行可以节约资源,提高系统性能
2.线程比进程的粒度更细,调度成本更低:由于线程是进程下的一个执行流,线程占用的资源,拥有的数据是少于进程的,所以说线程的粒度比进程的细。由于线程与进程使用同一份页表和地址空间,所以同一进程下的线程在创建和切换时不用关心页表和地址,调度成本也就更低
3.线程是CPU调度的基本单位:线程是系统调度的基本单位,进程是系统分配资源的基本单位,操作系统为进程分配资源,在调度执行流时就使用进程下的线程,以线程为基本单位进行调度
虽然说Linux下没有真正的线程概念,但是查找Linux源代码,在struct task_struct中可以找到一个结构struct thread_struct,该结构就是线程的控制块。
可以看到thread_struct中有sp,es,ds,这些东西都是寄存器的地址,这些寄存器维护了线程在进程中使用的地址,维护了线程的栈结构,是线程所占用的资源。换句话说,严格意义下,Linux并不是没有真正意义上的线程的,即使它的设计没有其他真线程操作系统复杂,但是线程结构体在Linux中是存在的。
Linux线程和进程的比较
提到进程,我们侧重谈论的是进程的资源属性,即进程是承担分配资源的基本单位。提到线程,我们侧重的点则是线程的执行属性,线程是系统调度的基本单位,线程属于进程,与进程使用同一份地址空间,页表,它们使用相同的资源,但是线程也有自己的一部分数据
线程单独享有的数据:
1.线程ID:每个线程都有自己的LWP,这是肯定的
2,一组寄存器:从Linux的源码中可以看出,线程拥有自己的寄存器,以维护属于自己的资源,同时不访问到其他线程的资源,做到资源的独立性
3.函数栈:线程以调用函数的方式保证资源的独立性,线程运行起来后位于一个函数栈中,每个线程都有自己的栈结构,这个栈结构由自己的寄存器维护
4.errno:线程调用函数出错,或者陷入异常时,需要设置errno,如果errno不是线程自己的,而是共享的,那么当线程出现异常时,错误要怎么定位?因此errno必须是线程独享
5.信号屏蔽字:每个线程都可以设置自己对于信号的屏蔽
6.调度优先级:每个线程的调度优先级都不相同
线程与进程共享的资源:
1.文件描述符表:进程打开的文件对于线程是可见的,反过来也是同理
2.每种信号的递达方式(handler):进程的handler表会影响线程,反过来也是同理
3.当前工作目录:线程与进程在同一个工作目录下运行
4.用户id和组id:创建可执行文件的owner,以及文件的所属组,线程与进程相同
线程的优点
1.操作系统创建新=线程的代价比创建进程小得多,因为不需要创建页表,地址空间,以及进程的其他结构
2.切换线程的速度也比进程快,进程需要加载页表,加载地址空间,页表只需要在同一进程空间下切换地址(切换其他资源)
3.线程占用的资源少于进程,线程是进程的一部分,占有的资源当然少于进程
4.充分利用多处理的可并行数量
5.等待慢速IO结束时,程序可执行其他任务。进程可以使一个线程等待IO的结束,其他线程执行其他任务,如果没有线程,进程就必须等待IO结束才能执行其他任务
6.在IO密集型应用中,线程可以等待不同的IO操作结束,如果进程需要与多个IO设备进行IO操作,可以指派多个线程进行IO操作,多个线程等待IO结束,等待时间重叠,这也是一种节省时间
线程的缺点
1.健壮性较差:一个线程的异常将导致进程的异常,最终进程退出,所有的线程都无法运行,与进行相比,由于进程具有独立性,进程具有自己的地址空间,页表,一个进程的异常可以不会影响到其他进程
2.调试难度大:多线程的程序非常不好调式
Linux下线程的使用
线程的创建与销毁
由于Linux没有线程的概念,所以系统没有提供像进程那样正式的接口,使用系统接口创建线程的接口非常复杂(如clone函数,需要我们自己为线程分配栈空间,并将其首地址作为参数传入),因为创建线程的系统接口用起来实在是不友好,所以第三方线程库pthread诞生了,pthread库封装了复杂的系统接口,我们可以通过成本较低的方式使用pthread库函数,由于该库是第三方库,所以需要我们在编译源文件时要把-l pthread选项带上
关于线程的创建:pthread_create函数
thread:线程的id号,是一个输出型参数
attr:关于线程的属性,现在我们不用关心,将其设置为nullptr即可
start_routine:线程执行的函数,通过执行的函数使线程占有进程的不同资源
arg:执行函数需要传入的参数
线程作为进程下的执行流,不同的线程占用进程的资源不能冲突,所以我们通过使线程执行不同的函数,使不同线程占用不同的资源(就算执行同一函数,不同线程创建的栈帧也不相同,占用的资源也就不冲突了)
等待一个线程的返回:pthread_join函数
thread:等待的线程的线程id
retval:线程的返回数据,输出型参数,不需要则将其设置为nullptr
// mythread.cc
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>
#include <sys/types.h>
using namespace std;
void *callback1(void *arg)
{
string name = (char *)arg;
while (1)
{
cout << name << ":" << getpid() << endl;
sleep(1);
}
}
void *callback2(void *arg)
{
string name = (char*)arg;
while (1)
{
cout << name << ":" << getpid() << endl;
sleep(1);
}
}
int main()
{
pthread_t tid1;
pthread_t tid2;
// 创建线程tid1和tid2
pthread_create(&tid1, nullptr, callback1, (void*)"thread 1");
pthread_create(&tid2, nullptr, callback1, (void*)"thread 2");
while (1)
{
cout << "我是主线程" << getpid() << endl;
sleep(1);
}
pthread_join(tid1, nullptr);
pthread_join(tid2, nullptr);
return 0;
}
线程是进程下的执行流,主线程与新线程的执行互不冲突,也就是说上面的demo在1秒内将打印三条语句,因为主线程创建完新线程后,新线程就开始运行,并且主线程继续向后运行,三个线程同时运行
由于主线程与新线程之间缺少访问控制,我们不知道哪条语句会先输出,所以打印的结果中可能存在混乱的输出。
与进程一样,如果子进程退出了但没有人回收,子进程将一直处于僵尸状态,造成资源的泄漏。虽然新线程没有僵尸状态,但是如果新线程执行完成,主线程却没有回收其资源,同样会造成内存泄漏,所以只要创建了新线程就必须记得回收它的资源。
由于线程是位于进程下资源,所以线程没有PID,只有进程才有PID。通过ps -aL可以查看系统中运行的线程,LWP(light weight process)轻量级进程编号,可以理解为Linux下的线程id
PID和LWP相同的线程为主线程,两者不同的线程为新线程,一个进程至少有一个线程,我们称这个线程为主线程,新线程是被主线程创建的其他线程
关于pthread_join的第二个参数retval:这个参数的设计很有意思,我个人认为它有点像C语言下的万能参数,如果一个变量的类型为void*,表示指向不定类型的指针,在Linux环境下,void的大小为8字节,虽然说void是一个指针,存储的是一个地址,但是void*存储的没有必要一定是一个地址,它还可以是int,char甚至是一个字符串,只要大小不超过8字节,我们就可以通过对其强转能得到里面的数据。
所以将void**作为一个函数的形参,就意味着该函数可以修改void的值,因为形参是void的地址,这就是这个设计巧妙的地方。因此我们就可以在主线程定义一个void*变量(假设该变量名为ret),将其地址作为参数传给pthread_join函数,ret的数据就会变为线程返回的消息,而线程返回的消息是由我们设置的,我们当然知道返回的消息数据类型是什么,所以我们通过强转就可以得到线程返回的消息。
线程退出的三种方式
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>
#include <sys/types.h>
using namespace std;
void *callback(void *arg)
{
string name = (char *)arg;
int cnt = 5;
while (cnt)
{
cout << name << ":" << getpid() << endl;
cnt--;
sleep(1);
}
// 线程返回10,10的数据类型为long long,大小为8字节
return (void*)10;
}
int main()
{
pthread_t tid1;
pthread_create(&tid1, nullptr, callback, (void*)"thread 1");
int cnt = 7;
while (cnt)
{
cout << "我是主线程" << getpid() << endl;
cnt--;
sleep(1);
}
// 在主线程创建类型为void*的变量ret
void* ret = nullptr;
// 将ret的地址作为第二个参数传给pthread_join函数
pthread_join(tid1, &ret);
// 通过对ret变量的强转,我们就能得到线程返回的消息
cout << (long long)ret << endl;
return 0;
}
上面的demo中,主线程只有一个新线程,新线程将在5秒后退出,主线程在7秒后回收新线程的资源,并通过ret变量接收其返回消息
除了直接return返回线程的消息,pthread_exit库函数也能退出一个线程并返回退出信息
此外,pthread_cancel库函数会向线程id为thread的线程发送取消的请求,如果线程接收到该请求并返回,其返回结果是-1
修改主线程的代码,使主线程在3秒后执行pthread_cancel函数,向唯一的新线程发送取消请求,此时新线程会退出并返回消息
在聊进程的退出状态时,我提到进程有三种退出状态
代码跑完,结果正确
代码跑完,结果不正确
代码异常
pthread_join的retval参数最多只能表示两种状态:代码跑完的结果是否正确,当代码异常时,线程退出,线程就无法返回退出信息。但是终止线程异常的信号要怎么传递出去?仔细一想,线程是进程下的一个执行流,当线程陷入异常,进程当然也会陷入异常,所以进程也就退出了,我们就可以通过进程的退出得知线程的异常。
什么是线程ID?
pthread_self:关于线程的id,该函数返回当前线程的id,类型为pthread_t,但是它的值与LWP是不相同的
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>
#include <sys/types.h>
using namespace std;
void *callback(void *arg)
{
string name = (char *)arg;
while (1)
{
cout << name << ":" << pthread_self() << endl;
sleep(1);
}
}
int main()
{
pthread_t tid1;
pthread_t tid2;
// 创建线程tid1和tid2
pthread_create(&tid1, nullptr, callback, (void*)"thread 1");
pthread_create(&tid2, nullptr, callback, (void*)"thread 2");
while (1)
{
cout << "我是主线程" << pthread_self() << endl;
sleep(1);
}
pthread_join(tid1, nullptr);
pthread_join(tid2, nullptr);
return 0;
}
上面的demo将对每个线程的ID进行打印,运行程序后,通过结果可以看到每个线程的ID都不一样,并且这个数据值非常的大,与LWP完全不同,那么这个线程ID究竟是什么呢?
首先要明白一个观点,Linux是没有真线程的,Linux只有轻量级进程,所以Linux没有对外提供创建线程的接口,只提供了创建轻量级线程的接口,但是通过系统创建的轻量级进程,只是得到了一个执行流,这个执行流还有很多属性要设置,比如是谁创建的,拥有哪些资源,现在的状态…对于进程,系统提供的接口会设置进程的所有属性,对于轻量级进程,这些属性需要我们自己设置,轻量级进程的结构需要我们自己维护,创建一个轻量级进程后要做非常多的管理工作,这使得轻量级进程的使用成本异常的高,所以有人编写了线程库,线程库会提供创建线程的接口,创建的线程在底层封装了Linux的轻量级进程,并且线程库的接口会设置轻量级进程的所有属性,线程库接口的使用十分简单。
所以我们使用的线程必须基于第三方线程库,有了线程库我们才能高效的使用线程。而线程库作为一个动态库,在使用线程库时,操作系统会将其加载到内存中,然后映射进进程的共享区,而一个进程下会有很多的线程,线程库要管理这些线程就需要先描述这些线程,线程库使用struct thread_info结构体描述一个线程的信息
struct thread_info
{
void* stack; // 线程栈
...
}
该结构体中的一个成员是线程栈的首地址,线程栈是属于该线程的资源,线程的运行就是在这个线程栈上。回到最开始的问题,线程ID是什么?线程ID的数值为什么会这么大?其原因就是线程ID是指向这些结构体的起始地址,操作系统通过该地址找到一个线程,进而进行线程的调度
void *callback(void *arg)
{
while (1)
{
printf("%s: 0x%x\n", arg, pthread_self());
sleep(1);
}
}
int main()
{
// 栈区资源
int a = 10;
cout << "栈区资源地址:" << &a << endl;
// 堆区资源
int* p = new int(1);
cout << "堆区资源地址:" << p << endl;
pthread_t tid1;
pthread_t tid2;
// 创建线程tid1和tid2
pthread_create(&tid1, nullptr, callback, (void*)"thread 1");
pthread_create(&tid2, nullptr, callback, (void*)"thread 2");
while (1)
{
printf("我是主线程: 0x%x\n", pthread_self());
sleep(1);
}
pthread_join(tid1, nullptr);
pthread_join(tid2, nullptr);
return 0;
}
再写一段demo,创建一个栈区资源,一个堆区资源,分别打印两个资源的地址,再创建两个新线程,打印主线程与新线程的地址,观察它们之间的关系
在一个进程的地址空间中,堆区地址较低,向高地址增长,栈区地址较高,向低地址增长,两者相对而生,在堆栈之间有一块区域叫做共享区,存储着动态库,共享内存等共享资源。通过结果也能看到栈区的地址明显高于栈区地址,从主线程到线程1和线程2,不论是哪个线程,它们的线程ID都小于栈区资源的地址,而大于堆区资源的地址,所以我们可以猜测线程库创建的线程结构体位于共享区。而这些地址从主线程到线程1再到线程2是逐渐递减的, 我们可以猜测共享区的增长方式是向下的,和栈区相同
修改demo,打印出线程中的栈区资源与堆区资源的地址
可以看出线程中的栈区资源和堆区资源地址竟然都是和进程的栈区地址相近
线程局部存储
在一个全局变量前加上__thread,使该变量变为当前线程的私有数据,这就是线程的局部存储
// 全局资源
int global_val = 50;
void *callback(void *arg)
{
while (1)
{
printf("%s: global_val:%d, &global_val:%p\n", arg, global_val++, &global_val);
sleep(1);
}
}
int main()
{
pthread_t tid1;
pthread_t tid2;
// 创建线程tid1和tid2
pthread_create(&tid1, nullptr, callback, (void*)"thread 1");
pthread_create(&tid2, nullptr, callback, (void*)"thread 2");
while (1)
{
printf("我是主线程: 0x%x\n", pthread_self());
sleep(1);
}
pthread_join(tid1, nullptr);
pthread_join(tid2, nullptr);
return 0;
}
demo将打印每个线程的global_val值与其地址,由于进程与线程共享全局数据,所以猜测每个线程的全局资源global_val的地址相同,一个线程对global_val的++会影响其他线程的global_val值
全局变量被线程共享,每个线程打印的global_val地址相同,global_val的值也被不断++
在添加了__thread的修饰后,每个线程的global_val地址都不相同,一个线程对其的++不会影响其他线程的global_val值,因为它被__thread修饰,是线程的私有资源。
线程分离
一般情况下,创建的一个线程具有joinable属性,线程退出后,我们需要调用pthread_join,join线程,而join线程的意义在于1.回收线程的资源,2.接收线程的返回信息。由于资源的释放是必要的,但是接收线程的返回信息不是必要的,所以当主线程不关心线程的返回信息时,就只要关心线程的资源释放,我们可以调用pthread_detach分离线程,线程被分离后,主线程不再关心新线程的运行,当新线程运行完系统自动回收其资源。
pthread_detach函数只需要传入线程ID,即用pthread_self返回的线程ID,就能实现对特定线程的分离。分离线程时,最好是由主线程分离新线程,而不应该有新线程分离自己。除了pthread_self的返回值,pthread_create有一个输出型参数,通过该参数也可以得到新线程的ID,主线程调用pthread_create创建新线程后就知道了新线程的ID,通过线程ID,主线程就可以分离特定线程。
如果线程被分离,再调用pthread_join将导致函数调用出错,函数返回错误码
void *callback(void *arg)
{
pthread_detach(pthread_self());
cout << "线程被分离" << endl;
while (1)
{
printf("%s: 0x%x\n", arg, pthread_self());
sleep(1);
}
}
int main()
{
pthread_t tid1;
pthread_t tid2;
// 创建线程tid1和tid2
pthread_create(&tid1, nullptr, callback, (void*)"thread 1");
pthread_create(&tid2, nullptr, callback, (void*)"thread 2");
int ret = pthread_join(tid1, nullptr);
cerr << strerror(ret) << endl;
ret = pthread_join(tid2, nullptr);
cerr << strerror(ret) << endl;
while (1)
{
printf("我是主线程: 0x%x\n", pthread_self());
sleep(1);
}
return 0;
}
这段demo的分离由新线程自己分离,而主线程调用pthread_join函数,由于线程被分离,这将导致join的错误,cerr会输出错误信息
但是通过运行截图,可以看出虽然线程被分离,但主线程调用join却没有出错,此时的主线程应该阻塞在第一个join函数,等待thread1线程的退出,所以后面的while循环打印没有执行
主线程创建完新线程后,新线程的调度还需要一些时间,在新线程的调度前主线程执行了join函数,此时发现新线程正在正常的运行,所以主线程进入阻塞,等待新线程的退出,在主线程陷入等待后,新线程才被分离,此时新线程继续运行,但是由于新线程没有设置退出的代码,所以只要进程没有退出,这个线程就会一直存在,导致内存泄漏
要解决这个内存泄漏,可以将主线程休眠一段时间,主线程唤醒后,新线程已经被分离,此时join就会失败
但是这样的解决方法依然不推荐,最规范的做法是由主线程分离新线程,而不是由新线程分离自己。如果由新线程分离自己,主线程不知道新线程的具体分离时间,所以主线程会与新线程有一段时间的联系才会被分离,在这段时间中或许就会产生bug。而由主线程分离新线程就可以确定新线程的具体分离时间,可以很好的把控代码的编写。
线程被分离后,意味着主线程不再关心线程的状态,但是主线程一旦退出,整个进程也就退出了,所有的线程也随之退出,包括被分离的线程,所以一般进行线程分离时,主线程需要一直运行(因为主线程不知道新线程的什么时候退出,主线程的退出需要在新线程之后,所以主线程的退出时间是不确定的,也就需要一直运行了)
exit对于线程的影响
exit用来退出整个进程,当一个线程调用了exit,该线程所属的进程就会退出,而进程下的所有线程也会随进程的退出而退出
在线程的执行中加上exit,该线程的退出将影响整个进程
可以看到整个进程退出,并且进程的退出码与线程的exit传入的参数相同