[Linux]多线程(一)充分理解线程库
标题:[Linux]多线程
@水墨不写bug
文章目录
- 一、线程的概念
- 1、一句话总结区分进程和线程
- 2、如何理解?
- 3、那么进程和线程的对比?
- 4、Linux为什么要这样设计进程和线程,难道不乱吗?
- 5、从CPU的角度看待执行流?- - - 统一看待
- 二、辩证的看待线程
- 1、已经有多进程了为什么还要实现多线程?线程的优势?
- 2、线程也有自己的劣势
- 线程的缺点
- 线程异常
- 三、深刻的理解:进程的虚拟地址是一种资源,被线程占有
- 四、线程之间共享和独立性
- 共享的数据
- 独立的数据
一、线程的概念
1、一句话总结区分进程和线程
在学习OS的时候,如果要完成一件任务,你的第一反应一定是创建一个线程。
什么是线程?
线程在进程内部运行,是CPU调度的基本单位。
而进程呢?
进程=内核数据结构+代码和数据,是系统资源分配的基本单位。
2、如何理解?
进程是系统分配资源的基本单位:系统创建一个进程,需要创建进程的内核数据结构(包括进程PCB“task_struct”,进程地址空间**“mm_struct”**,页表等)+代码和数据(如加载 文件描述符表)。
下图体现了一个进程的大部分资源:
如果要同时执行多个任务,那么就需要创建多进程,创建多进程需要给每一个进程分别分配资源(这就体现了进程是资源分配的基本单位):
但是创建多进程的资源消耗还是太大了,那么就可以通过线程来缓解资源消耗过大的问题。
创建一个进程,需要创建一批进程的资源;而创建线程,只需要在进程的基础上,创建一个task_struct就可以了:
当我们创建多线程,通过:ps -axj | head -1 ; ps -axj | grep mytest
查询到进程还是只有一个,因为这多个线程是属于同一个进程。自然只有一个进程pid;而想要查询线程id,需要:
ps -aL
查询出来有多个线程,每一个线程都有线程id:tid。其中主线程id最小.
主线程的tid等于整个进程的pid(可以用这个条件来判断此线程是不是主线程)。其他线程(新线程)id依次递增。
CPU在调度的时候,是根据tid来区分线程的。从CPU的角度,调度的对象都是task_struct。因此,线程是CPU调度的基本单位。
3、那么进程和线程的对比?
之前我们讲的进程,其实就是只有一个task_struct的进程。
有一个或者多个task_struct的都是进程。
一句话总结:
进程:内部所有的task_struct+mm_struct+页表+代码和数据;
线程:一个task_struct+部分mm_struct+部分页表+部分代码和数据。
(后文将会解释为什么)
4、Linux为什么要这样设计进程和线程,难道不乱吗?
线程也需要先描述再组织,需要创建、暂停、销毁、调度、最终需要被管理起来。但是,已经存在的进程已经设计了进程PCB,调度算法。为什么不复用?于是,为了简化代码的设计逻辑,也便于后期维护,Linux线程复用进程的代码,使用进程来模拟线程。一般来说,进程和线程是两种不同的数据结构,有两种不同的调度算法。如图:
其实,在Windows下,确确实实 的实现了线程自己的控制块,有自己的创建线程和创建进程的系统调用,线程没有复用进程的代码,但是这会导致代码的复杂程度提高,出BUG概率提高,后期维护成本高的问题。因而Linux的线程复用进程的代码是更优的选择。
5、从CPU的角度看待执行流?- - - 统一看待
CPU调度的时候,得到的就是一个task_struct
,CPU不管这个task_struct
是什么,它只管调度
。只管从task_struct
的mm_struct内获取页表,读取PC指针,然后地址转换,获取数据,只管调度执行相应的执行流。
那么 CPU看到的task_struct <= 进程。
由于CPU看到的都是同一种东西,task_struct
;于是把Linux下的CPU看到的这个task_struct
以及其所包含的代码和数据称为 轻量级进程(LWP)
。
Linux系统没有线程的概念,只有轻量级进程。Linux通过轻量级进程来模拟实现线程!!那么Linux向上层提供的系统调用也是管理LWP的一套函数。
跨平台的用户在使用的时候,不认识 轻量级进程(LWP)的概念,用户只认识创建线程,终止线程,调度线程,等待线程。
于是,Linux在用户与系统调用之间实现了一个软件层——pthread库(Linux的原生线程库)这个线程库对LWP的一套接口进行封装,按照线程接口的方式,交给上层用户使用。
于是,Linux的线程是用户级线程;Windows的县城是内核级线程。
二、辩证的看待线程
线程有自己的优势,当然也有缺点。
1、已经有多进程了为什么还要实现多线程?线程的优势?
进程创建的成本非常高,创建一个进程,不仅需要创建task_struct
,还要创建mm_struct,页表等,还要加载代码和数据。
创建线程只需要创建一个task_struct
,其他的如mm_struct,页表,代码和数据已经有了,不需要创建了,把task_struct
和已有的这些数据关联起来就可以了。
多线程创建成本低!
在运行时候,CPU为了加快整体访问内存的效率,当我们访问一部分数据的时候,CPU会同时往CPU内部的**Cache(缓存)**中加载一部分数据,称为“热数据”,这样做遵循了“局部性原理”。CPU在下一次访问内存之前,会先在Cache中查找是否有目标数据,如果命中,则省去了访存操作;如果没有命中,则进一步访存。
可以通过
lscpu
查看Cache信息:
不要忘了,进程间相互独立,代码和数据自然不一样!同一个进程的线程间共享代码和数据!
这就导致:
线程之间切换调度,Cache热数据命中率高;进程之间切换调度,Cache热数据命中率低。
线程A加载的热数据,CPU切换到线程B之后,线程B也可以用上。进程C加载的热数据,CPU切换之后,进程D还需要重新加载Cache,每一次都用不上。这就导致多线程与多进程之间的性能差异。
多线程运行成本低!
在线程销毁的时候,只需要销毁task_struct
即可,不需要销毁其他的数据结构和代码与数据,销毁成本低。
多线程销毁成本低!
2、线程也有自己的劣势
线程的缺点
性能损失
一个很少被外部事件阻塞的计算密集型线程往往无法与其他线程共享同一个处理器。
如果计算密集型线程的数量比CPU的核数多,那么可能会有较大的性能损失(这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变)
健壮性降低
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,线程之间是缺乏保护的。因为多线程可以共享一些数据(比如全局变量),索引需要考虑进一步控制线程之间的同步和互斥来解决线程安全问题。
缺乏访问控制
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
Debug难度提高
编写与调试一个多线程程序比单线程程序困难得多。
线程异常
单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃。线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程。进程一旦终止,该进程内的所有线程也就随即退出。
如果多线程的代码设计有问题,健壮性很差。但是多进程却没有这样的问题(因为进程间具有独立性)。
三、深刻的理解:进程的虚拟地址是一种资源,被线程占有
一个程序,在编译的时候是被统一编址的。
一个线程被创建之后,去执行一个函数,本质就是这个线程拥有的整个进程的一部分代码和数据!!这个线程等于是说拿了一小部分虚拟地址空间的范围!!就等于说这个线程拿了整个进程的页表的一小部分!!等于说是这个线程使用了一个进程的一部分资源!!!
一个线程,只会执行这一个函数(函数是一块地址空间),只拥有这个函数对于虚拟地址块,只会使用对应页表的一部分,只会映射到一部分物理地址。
四、线程之间共享和独立性
共享的数据
进程的多个线程共享 同一地址空间,因此Text Segment、Data Segment都是共享的。
如果定义一个函数,在各线程中都可以调用;如果定义一个全局变量,在各线程中都可以访问到。
除此之外,各线程还共享以下进程资源和环境:
- 文件描述符表
- 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
- 当前工作目录
- 用户id和组id
独立的数据
线程之间并非所有数据都是共享的。线程共享进程数据,但也拥有自己的一部分数据:
- 线程ID
- 一组寄存器(线程是动态运行的,有上下文数据)
- 栈(在线程运行时,会形成临时变量;在线程执行函数内,还可能会调用其他的函数,需要栈来维护函数栈帧)
- errno
- 信号屏蔽字
- 调度优先级
完~
转载请注明出处