Linux多线程详解

简介: 本节内容,我们将详细讲解Linux线程的有关知识,并为同学们铺垫多线程的有关知识。

本节内容,我们将详细讲解Linux线程的有关知识,并为同学们铺垫多线程的有关知识。


一、线程的概念和理解

理解线程之前,我们需要重新对进程进行理解


我们前面说一个task_struct有着一个进程地址空间,然后有页表,搭建其和物理内存的映射。


我们现在需要重新对进程进行定位理解:


(1)对于CPU来说,它在运行一个进程的时候,其看到的PCB可以是多个,然后共享一份地址空间。(说是PCB其实不够准确,应该叫TCB,但是和PCB非常相似,这里为了引出线程,暂时可以当PCB这样理解)


注意,这里你没有看错,就是多个PCB共享同一块地址空间。允许这样的情况存在。


image.png


如上图。


(2)我要提出问题了,那如果这样的话,进程到底是啥呢?


我们之前说进程是一系列的代码和数据以及组织它的数据结构,并且这主要的数据结构就是PCB。为什么会有多个PCB?


那现在呢?这个说法难道要被推翻了吗?


实际上,我们不难发现,我们在进程创建的时候,会在同时创建一大批的系统资源:比如PCB、进程地址空间、页表等。而当存在这样一种情况:多个PCB共享同一块进程地址空间的时候,就会有如上图中三个进程控制块同时指向code区域中的三份不同的代码的情形。我们是允许这样的情况出现的。


希望上述的例子你能够接受。可以不用理解,接受即可。


(3)那我们现在,对线程再做一下定义,线程到底算是什么呢?


我们说,进程是承担分配系统资源的基本实体。


那么线程,是系统调度的基本单位。(线程是在进程的地址空间内运行的)


我们之前说进程是系统调度的基本单位,实际上是不够准确的。


应该是线程。线程是进程的执行流!!!


上面的每一个PCB,都可以认为是一个线程(说法不准确,仅为了在此引出线程、方便理解,阶段性认知错误是必要的哈哈哈)。


所以,一个大的进程内部,是可以有多个线程的。


我们的每一个执行流并不是一个个进程。而是一个个线程。


而我们之前所学的,都是一个进程有一个线程的。即单线程的简单的执行流。


ps:还需要注意的是,在Linux中,是没有真正意义上的线程(注意,这里指的是Linux没有为其专门开辟新的数据结构)。而线程是用进程来模拟的。


为什么呢?线程和进程实际上是拥有许多相同属性的。如果我们再为线程专门去开辟对应的结构,那么调度关系就会变得相对复杂,而复杂的关系一般不会太高效。(不过Windows还是有专门的线程库的)


我们以后该怎么面对PCB呢?


我们依旧讲PCB叫做进程控制块。


不过,如果在一个进程中,如果只有一个PCB,那么该进程就是拥有单线程的进程。


如果有多个PCB,那么其就是拥有多线程的进程。


而所有的PCB/线程,都可以叫做轻量级进程。


结合上面的,我们来总结一下何为线程:


在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”,即更准确地来说,其是在一个进程地址空间内运行的执行流。


所以说,一切进程至少都有一个执行线程。而线程在进程内部运行,本质是在进程地址空间内运行。


在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化。透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流。


线程的优点

1、创建一个新线程的代价要比创建一个新进程小得多


2、与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多


3、线程占用的资源要比进程少很多


4、能充分利用多处理器的可并行数量


5、在等待慢速I/O操作结束的同时,程序可执行其他的计算任务


6、计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现


7、I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。


注意:上述进程的优点的后四点也是进程的优点。


虽然Linux操作系统没有为我们提供多线程的库,但是呢,作为用户,我们又要用到多线程,所以,我们就用基于LInux系统开发的第三方库——pthread库  


1)创建线程接口:pthread_creat


注意,编译链接的时候要加上 -pthread(如下图)



a、这里的第一个参数为线程id(就是我们后面要说的lwp),这个参数OS实际上并不知道,其是基于用户层面上开发的。但是呢,在运行到内核的时候,我们的OS的PCB会与这里的lwp存在着某种对应关系。


就是说,线程的管理和组织不再由OS来管控,而是由用户层的库来管控。我们“先描述、再组织”的方式是放在库里面的了。(稍后我们再来看库里面有什么东西)


b、第二个参数表示线程属性,我们暂且不关心。


c、第三个参数表示线程执行流,说白了,就是去调用这个函数。通过这个函数来去完成新线程的执行流。


d、第四个参数表示第三个函数的参数。


其返回值成功就为0,若出错,就返回相应的错误码。


我们来看下面的一个例子:


微信图片_20221210194259.png


原码:

1 #include  
  2 #include  
  3 #include  
  4   
  5   
  6 void* thread_run(void* arg)  
  7 {  
  8   while(1)  
  9   {  
 10     printf("I am %s\n",(char*)arg);  
 11     sleep(1);  
 12   }  
 13 }  
 14   
 15 int main()  
 16 {  
 17   pthread_t rid;  
 18   pthread_create(&rid, NULL , thread_run , (void*)"thread 1");  
 19   
 20   while(1)  
 21   {  
 22     printf("I am main thread\n");
 23     sleep(2);                                                                                      
 24   }
 25    
 26   return 0;
 27 }


运行截图:



可以很容易地发现,我们这里是有两个执行流的。


我们可以通过ldd,来查看我们的这个可执行文件链接的动态库。


如上,我们可以看到,其链接的是一个pthread的动态库。 (与我们刚刚在编译的时候的-phread相对应)


那怎么证明我们的两个执行流(线程)是一个进程里的呢?


我们先来认识一个函数:pthread_self



其作用很简单,其是获得当前线程的id


我们将上面的代码改一下:





那么现在,我们对于线程的理解,可以这样来说明了:



image.png

我们前面所说的结构体、控制块都是由OS创建并完成。


但是线程由于Linux操作系统没有为其有专门的线程库,我们又要用


所以就在用户层面, 由用户链接第三方库。这个第三方库叫做pthread库


在这个第三方库中,其会有模拟线程描述的结构体,叫tcb,它和PCB功能类似,它用来描述线程和组织线程,也叫做线程实体。


每一个这样的tcb中里面会有一个用来标识线程唯一性的lwp,它将来会以某种关系和PCB连接起来。


而每一个线程实体和Linux内核中的每一条执行流又是一一对应的关系。


所以说,OS为我们提供的是线程的执行功能,而线程的管理和组织是由用户空间来提供的。


来换一张图更加细致地说明:

image.png



左边是我们的进程地址空间,右边是我们链接的动态库


在外面的共享区,会有着pthread维护的结构体,遵循先描述、再组织,每一个结构体当中又会有着tid,线程栈这样的东西。


tid可以认为是该线程控制库在地址空间里的起始地址。


注意,在进程地址空间的栈是主线程栈!其他线程的栈是要在自己的线程栈中的,要不然数据就乱了。


我们解释完毕,再来说说一些其他的基本概念:


线程异常:

单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃


理由:线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出。


因为信号处理的基本单位是进程!所以线程的健壮性不强。


但是,合理的使用多线程,能提高CPU密集型程序的执行效率


合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)


二、线程TID的内容

我们前面说到,进程是资源分配的基本单位,线程是调度的基本单位。


虽然说线程共享进程数据,但也拥有自己的一部分数据:(即下面的数据是线程自身私有的)


线程ID

一组寄存器(上下文)(重要)

栈(重要)

errno

信号屏蔽字

调度优先级

进程的多个线程共享 同一地址空间,因此Text Segment、Data Segment都是共享的,


如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到。


除此之外,各线程还共享以下进程资源和环境:(即下面的资源是各个线程共享的)


文件描述符表

每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)

当前工作目录

用户id和组id

我们接着我们之前的代码,来说说线程退出和等待


1、主线程退出,整个线程退出。

因为进程是承担资源分配的基本实体。当主线程退出的时候,整个进程全部退出。整个进程都不存在了,自然所有的线程就都不会再存在了。

image.png



2、线程退出需要等待        

pthread_join()

image.png



第一个参数是需要等到的线程ID,第二个参数是一个一级指针的接收列表。


什么意思呢?


我们还是按照刚刚的例子来说:(稍微改动一下)


微信图片_20221210194732.png


我们通过pthread_join,来将线程tid的返回值拿了回来,然后再将其打印出来。


我们将其返回值打印了出来。


我们来运行看一下:(其确实拿到了返回值10)



如果不去进行等待,就可能会造成类似于僵尸进程的情况,即我们新创建出来的PCB没有被立即回收。


那么结束线程的方式,我们可以再来总结补充一下都有哪些。


1、函数执行完了,线程结束;


2、通过pthread_exit()函数;该函数的用法和进程当中的exit是相同的。


3、pthread_cancel()函数,从外部将该线程杀掉。即直接在别的线程中调用pthread_cancel(tid)即可。有种人在家中坐,锅从天上来的感觉。



与进程不同的是,对于线程,我如果不想要等待,让这个线程自己玩自己的,我们可以让其分离。


用pthread_datch()函数。


来解释一下:


1、默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。


2、如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。(注意,这个时候,我们也是不会再拿到退出码的了)


3、分离的时候,就直接pthread_detach(pthread_self());就可以了。



但是需要注意的是,二者还是在一个进程当中,如果有一个出问题了,还是整个进程都会崩掉的。





目录
相关文章
|
6月前
|
消息中间件 存储 缓存
【嵌入式软件工程师面经】Linux系统编程(线程进程)
【嵌入式软件工程师面经】Linux系统编程(线程进程)
127 1
|
4月前
|
算法 Unix Linux
linux线程调度策略
linux线程调度策略
81 0
|
2月前
|
资源调度 Linux 调度
Linux C/C++之线程基础
这篇文章详细介绍了Linux下C/C++线程的基本概念、创建和管理线程的方法,以及线程同步的各种机制,并通过实例代码展示了线程同步技术的应用。
30 0
Linux C/C++之线程基础
|
2月前
|
安全 Linux
Linux线程(十一)线程互斥锁-条件变量详解
Linux线程(十一)线程互斥锁-条件变量详解
|
4月前
|
存储 设计模式 NoSQL
Linux线程详解
Linux线程详解
|
4月前
|
缓存 Linux C语言
Linux线程是如何创建的
【8月更文挑战第5天】线程不是一个完全由内核实现的机制,它是由内核态和用户态合作完成的。
|
4月前
|
负载均衡 Linux 调度
在Linux中,进程和线程有何作用?
在Linux中,进程和线程有何作用?
|
4月前
|
缓存 Linux C语言
Linux中线程是如何创建的
【8月更文挑战第15天】线程并非纯内核机制,由内核态与用户态共同实现。
|
6月前
|
API
linux---线程互斥锁总结及代码实现
linux---线程互斥锁总结及代码实现
|
6月前
|
Linux API
Linux线程总结---线程的创建、退出、取消、回收、分离属性
Linux线程总结---线程的创建、退出、取消、回收、分离属性