文章目录
- 前言
- Q&A
- 线程概念
- Q:线程和进程的区别?(为什么要有线程,从进程的角度说明这个问题)
- Q:Linux是如何设计线程的?
- Q:学习了线程后,你能说说进程和线程最大的区别是什么吗?
- 进程和线程的比较
- Q:线程使用进程的资源,它们之间的所有资源都是共享的吗?有哪些资源不是共享的?
- Q:线程的优缺点分别是什么?
- 线程操作
- Q:线程终止的三种方式分别是什么?
- Q:为什么pthread_self()的返回值和LWP不一样?
- Q:为什么要进行线程分离?
- Q:为什么要由主线程分离子线程?不能子线程自己分离?
前言
本篇博客梳理关于线程相关的Q&A,包括了线程概念与线程的控制。若读者也在复习这块知识,或者正在学习这块知识,可以通过这些Q&A检测自己的知识掌握情况。此外,思维导图已经更新至我的gitee,Q&A之外的体系梳理还请移步思维导图。
Q&A
线程概念
Q:线程和进程的区别?(为什么要有线程,从进程的角度说明这个问题)
A:Linux用task_struct结构体描述一个进程,可以说进程 = task_struct + 内存中的数据与代码。而task_struct包含了进程页表、进程地址空间等资源,创建一个进程就需要为它分配这些资源。值得思考的是:分配资源本身就需要消耗资源,进程是否能充分利用分配得到的资源?而且由于进程具有独立性,进程间想要共享或发送数据,就要进行进程间通信,但通信的成本过高,会消耗过多资源,降低系统的性能。
若操作系统只有进程,那么进程就是系统的基本执行流。
- 要执行不同的程序,就要创建新的进程,但是创建进程会带来资源的消耗,这是其一
- 若一个进程要使用另一个进程的数据,就要进行进程间通信,通信的代价也是额外资源的消耗,这是其二
为解决以上两个问题(最主要的两个),线程被引入,线程作为进程的一部分,和进程共享进程地址空间(资源),同时解决了进程间通信与反复创建进程带来的资源消耗问题。
Q:Linux是如何设计线程的?
A:学习进程时,我们说:进程 = task_struct + 内存中的数据与代码,这个说法默认了task_struct是进程控制块,也就是说task_struct是为了进程而设计的结构。但是事实并不是这样,准确的说task_struct是Linux中的一个执行流。若一个进程下没有线程(或者说唯一的线程就是自己),此时的进程就是一个执行流。若一个进程下有多个线程,此时的进程就不再是执行流,此时的执行流是进程下的线程,进程是多个执行流的集合。
所以task_struct即不是为进程设计的,也不是为线程设计的,它是为执行流这个概念设计的,你也可以极端点,认为Linux下没有进程与线程的概念,Linux只有执行流的概念。提出进程和线程只是为了更好的理解操作系统。
回到问题,你可以认为Linux只有执行流的概念,它对应的结构体为task_struct。但是为了使多个执行流可以同时使用相同的资源而不冲突,Linux肯定是要对线程进行设计的。我们可以通过if else判断fork的返回值,控制父子进程,使两进程分别执行不同的代码块(函数),从而使它们的函数栈分离,体现在进程地址空间上,两进程就是使用了不同的空间。这样的思想也体现在线程的设计中,Linux设计了thread_struct结构体,其中有一组变量维护了寄存器地址,这些寄存器则维护了一个函数栈。所以,每个线程都享有一个独立的函数栈(地址空间),这样就解决了线程间数据冲突的问题。
总结一下:Linux用thread_struct结构体表示线程结构,该结构体中最重要的是一组寄存器地址,这些寄存器维护了线程的函数栈,使线程享有独立的资源,互不冲突。
Q:学习了线程后,你能说说进程和线程最大的区别是什么吗?
A:两者最大的区别就是:承担的职责不同
- 进程是系统中资源分配的基本单位,系统分配进程地址空间、页表等结构,消耗了大量资源
- 线程是系统中调度的基本单位,系统分配资源给进程,线程使用进程的一部分资源,以执行任务
进程和线程的比较
Q:线程使用进程的资源,它们之间的所有资源都是共享的吗?有哪些资源不是共享的?
A:
- 线程独享的资源
- 线程ID:需要用不同的线程ID标识同一进程下的不同线程
- 一组寄存器与函数栈:为了防止线程之间发送数据冲突,线程需要维护自己的函数栈
- errno:每个线程必须独享errno以便在程序崩溃时更快定位错误
- 信号屏蔽字:每个线程可以设置自己想要屏蔽的信号
- 调度优先级:可以设置线程的优先级以调整线程的执行顺序
- 线程与进程共享的数据
- 文件描述符表:线程和进程打开的文件,彼此都能看到
- 信号的递达方式:线程和进程设置的信号递达方式也会彼此影响
- 当前工作路径:进程与使用其资源的线程在同一工作路径下运行
- 用户id和组id:进程与使用其资源的线程拥有相同的owner和group
Q:线程的优缺点分别是什么?
A:
- 优点:
- 充分使用进程的资源,尽可能的减少不必要进程的创建,提高系统性能
- 线程之间数据共享,比起进程间通信,这是一种更高效的通信方式
- 创建线程的代价小于进程,因为系统不要为线程分配页表、进程地址空间这样的资源
- 切换线程的代价小于进程,因为系统不需要重新加载页表、进程地址空间,只需要重新加载task_struct结构体以及线程的函数栈
- 充分利用多处理器的可并行数量
- 在含有慢速IO的进程中,可以创建线程等待慢速IO的结束,使进程可以执行其他任务,不必等待慢速IO的结束
- 在密集IO的进程中,可以创建多个线程等待IO的结束,使等待时间重叠,有效提高了程序的运行效率
- 缺点:
- 健壮性较差:线程崩溃退出会导致进程的退出
- 调试难度大:多线程程序下的错误难以定位
线程操作
Q:线程终止的三种方式分别是什么?
A:
- 直接return,不过返回的对象要强转为(void*)
- 调用int pthread_exit(void* retval)退出,该函数的参数是一个类型为void*的变量
- 调用int pthread_cancel(pthread_t thread)向指定线程发送cancel信号,该线程的返回值为-1
Q:为什么pthread_self()的返回值和LWP不一样?
A:LWP(light weight process),轻量级进程,使用ps -aL可以查看线程的LWP。而pthread_self()的返回值(线程ID)远远大于LWP,其原因是:
-
Linux没有提供线程的操作接口,或者说这些接口不够简便,需要自己设置于管理线程的属性。我们的线程操作基于第三方库,第三方库帮助我们设置与管理线程的属性
-
第三方库使用struct thread_info结构体存储线程的信息,而这些结构存储在进程地址空间的共享区,线程ID的值就等于这些结构体的首地址
-
可以通过程序验证,线程ID的值小于栈区地址,大于堆区地址
-
LWP是内核的一个概念,内核用LWP对轻量级进程进行管理
-
线程ID是地址空间的一个概念,通过线程ID可以找到线程的属性,从而对线程进程管理(这是我们通过线程库对线程的间接管理)
Q:为什么要进行线程分离?
A:主线程创建的子线程默认具有joinable属性,若主线程不主动join子线程,会造成资源泄漏与线程句柄的耗尽。线程分离后,主线程不用join子线程,子线程结束,其资源会自动释放。
这个问题也能理解为:为什么要join子线程?两个原因:一个是回收子线程的资源,一个是得到子线程的返回值,前者是必要的,而后者是非必要的。当主线程不再关心子线程的返回信息时,主线程可以主动分离该线程。
Q:为什么要由主线程分离子线程?不能子线程自己分离?
A:这是为了防止一些bug的产生,也是一种编程规范。若子线程调用pthread_detach分离自己,主线程无法确定子线程什么时候被分离,如果主线程在在子线程调用pthread_detach之前调用pthread_join该线程,那么主线程会陷入阻塞,但由于该线程被分离,不会向主线程返回,所以主线程会陷入永久的阻塞,程序因此产生bug。
所以不能让子线程调用pthread_detach分离自己,这会带来一些不确定性。同时我们也要确保主线程不要对将要分离或已经分离的子线程做任何操作。