目录
一、进程基本属性回顾
二、线程概念
三、操作系统为什么要引入线程—多进程和多线程的区别
为什么多线程比多线程调度效率更快?
四、线程的优点
五、线程的缺点
六、线程异常
一、进程基本属性回顾
在学习线程之前,我们先来回顾一下进程的基本属性:
1、进程是一个可拥有资源的独立单位——即进程是系统资源分配的基本单位。一个进程能够独立运行,就必须拥有一定的资源,包括:存放程序正文和数据的磁盘、内存地址空间、运行时所需要的I/O设备、进程已经打开的文件、信号量等。
2、进程是一个可独立调度和分派的基本单位。每个进程在系统中均有唯一的PCB(在Linux系统中是task_struct),系统可以根据PCB来感知进程的存在,也可以根据PCB中的信息对进程进行调度,还可将断点信息保存在进程的PCB中 。反之,也可以利用PCB中的信息来恢复进程运行的现场。
正是因为进程具有以上基本属性,进程才成为了可以独立运行的基本单位,从而也构成了进程并发执行的基础。
二、线程概念
那么什么是线程呢?我们先引入如下概念:线程是进程的一个实体,是CPU调度和分派的基本单位!
到这里可能有同学会有疑问了:之前不是说进程是是一个可独立调度和分派的基本单位吗?现在怎么又说线程是CPU调度和分派的基本单位了?
其实在已经引入线程的操作系统中,就已经把线程作为CPU调度和分派的基本单位了!
在Linux系统当中,我们可以这么来理解进程和线程:进程是操作系统进行资源分配的基本单位,线程是CPU调度和分派的基本单位!
那我们如何理解之前所学的进程呢?我们之前所学习的进程中,其实只包含一个执行流,也就是一个线程!实际上,我们之前所学习的进程是:具有一个执行流的进程,而一个进程必须至少拥有一个线程!!!
举个更通俗的例子:在我们国家中,家庭通常是政府进行资源分配的单位,则家庭可以视为进程,而一个家庭至少包含一个家庭成员,也可包含多个,且家庭成员共享家庭的资源,但家庭成员又有各自的隐私,因此可以将家庭成员视为线程。家庭成员各自的任务和活动(如工作、学习、娱乐)可能会不同,但都在共享的家庭资源框架内进行。这类似于线程在进程中执行不同的操作,虽然每个线程可能执行不同的任务,但都在同一进程的资源和环境下运行。增加一个新的家庭成员(比如增加一个子女)相对容易,家庭的资源和管理模式不会发生重大改变。这类似于创建新线程的开销很小,因为新线程与现有线程共享资源和环境。相比之下,创建一个新的家庭(即进程)需要更多的资源和管理成本。
通过上图我们可以大致了解到:
1、在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”。
2、线程在进程内部运行,本质是在进程地址空间内运行。线程就是进程中的一个执行单元,是进程的一部分,一个进程至少有一个线程(主线程),可以创建多个线程来并行执行任务。
3、透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流。同一进程中的所有线程共享进程的资源,但它们的执行是独立的。线程有自己的执行路径,可以同时进行多项任务,从而实现并发。
4、进程的多个线程共享同一地址空间,因此代码段和数据段都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:文件描述符表、每种信号的处理方式、当前工作目录、用户 ID 和组 ID
【了解】:但是实际上,Linux操作系统并没直接创造“线程”这一具体的事物,而是通过创造“轻量级进程(Lightweight Process, LWP)”来实现线程的功能。
用户视角:Linux 使用 pthread(POSIX线程)库来提供线程创建和管理的接口。pthread 库为线程提供了标准化的 API,允许程序员创建和管理线程、进行线程同步等。
内核视角:在内核中,每个线程是一个进程,但具有某些共享资源。内核的调度器处理这些线程就像处理进程一样。
所以我们在Linux系统中编写程序时,我们所面对的是pthread库为我们提供的管理“线程”的方法,在内核中实际上是被当作轻量级进程(LWP)来处理的,因此我们称Linux中的线程为“用户级线程”。而在Windows操作系统中,内核为线程专门创造了TCB这一数据结构来对每个线程进行标识和管理,而这些操作都由Windows内核来进行管理,所以我们称之为内核级线程。
所以在Linux内核看来,对线程的调度与管理实际上就是对进程的调度与管理,依然可以沿用进程调度的那一套算法。而Windows则需要单独为线程创造一套专属的数据结构与方法。
这也是为什么在 Linux 系统中:在 CPU 眼中,看到的 PCB 都要比传统的进程更加轻量化!
三、操作系统为什么要引入线程—多进程和多线程的区别
实际上,线程的引入是为了更好的提高程序的并发程度。
在引入线程这一概念之前,程序的并发执行多由多进程进行实现。但是通过之前的学习我们知道:进程是系统资源分配的单位,当多个进程并发执行时,势必会为系统资源的分配带来压力!
使用多进程时,系统必须要进行如下操作:
1、创建进程:系统在创建一个进程时,必须为它分配其所必须的、除处理机以外的所有资源(如内存空间、I/O设备等),并创建对应的PCB。
2、撤销进程:系统在撤销进程时,必须先对其所占有的资源执行回收操作,然后再撤销PCB。
3、进程切换:对进程进行上下文切换时,需要保留当前进程的CPU环境,这一过程需要花费不少的处理机时间。
由于进程是资源的拥有者,因此在对进程进行管理时,操作系统必须付出较大的时空开销。这就限制了系统中所设置的进程的数目,而且进程的切换也不宜过于频繁,从而限制了程序并发程度的进一步提高。
因此为使多个程序能够更好的并发执行,同时又能减少系统的开销,线程就应运而生。线程的出现将进程的两个基本属性分割开来:从此进程仅作为拥有资源的基本单位,线程则成为了调度和分派的单位。因此,在使用多线程时,由于多线程共享所属进程中的资源,因此减少了资源的频繁切换。
但同时,线程也有属于自己的资源:
栈:每个线程都有自己的独立的栈,用于存放函数调用时执行上下文的数据,如局部变量、函数参数等。栈是线程特有的,可以确保线程之间不受其他线程的影响。
一组寄存器的状态:线程执行时所使用的寄存器,例如,程序计数器、寄存器组等,每个线程都有自己的寄存器状态,这是独立于其他线程的。
线程ID:每个线程都有自己的线程标识符(Thread ID),可以在同一进程中的其他线程或同级的用户程序中进行线程间通信或同步时识别和引用。
执行上下文:线程的执行上下文包含其调用的特定状态,如寄存器状态、指令指针等。这些状态在上下文切换时被保存和恢复。
线程局部存储(TLS):线程局部存储是特定于线程的全局变量,允许每个线程拥有一个独立的副本,但此全局变量必须为内置类型,且需要在变量类型前使用__thread关键字。
errno
:在多线程程序中,errno
通常被实现为线程局部存储(TLS),每个线程有自己的errno
副本,确保线程之间不会相互干扰。- 信号屏蔽字:每个线程都有自己的信号屏蔽字,它决定了该线程是否响应某些信号。线程可以通过系统调用(如
sigprocmask
)来设置或更改信号屏蔽字。- 调度优先级:每个线程都有一个调度优先级,操作系统根据这些优先级来安排线程的运行。高优先级的线程通常会比低优先级的线程获得更多的 CPU 时间。
如此,我们便不难回答:
为什么多线程比多线程调度效率更快?
1、共享资源:多个线程可以在同一个进程中共享资源,如堆栈、状态和数据。这意味着它们可以更快地访问同样的内存地址,减少了内存的访问时间。
2、上下文切换:线程的上下文切换通常比进程的上下文切换速度更快。因为线程共享了地址空间和大部分内核资源,所以切换线程所需的时间相比于进程要短。
3、缓存命中率:在多线程应用程序中,多个线程可以共享缓存中的数据,这可以提高缓存命中率。由于多个线程共享同样的数据,这意味着他们更有可能从高速缓存中读取数据,而不是从更慢的主存中读取。
缓存(Cache)是一种基于内存的高速数据存储技术,用于提高数据访问速度。缓存可以是硬件层面的,也可以是软件层面的。在计算机科学中,缓存主要用在CPU缓存中,即高速缓存。
4、减少系统调用:在多线程应用程序中,多个线程可以在同一个进程中共享相同的文件描述符和网络连接,这意味着它们可以重用同样的一些资源,而不是创建多个进程时必须重新打开资源。
5、减少地址空间开销:进程有它们自己的地址空间,而线程共享相同的地址空间,这意味着多个线程可以节省虚拟内存的开销。
四、线程的优点
• 创建一个新线程的代价要比创建一个新进程小得多
• 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多 • 线程占用的资源要比进程少很多
• 能充分利用多处理器的可并行数量
• 在等待慢速 I/O 操作结束的同时,程序可执行其他的计算任务
• 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实 现
• I/O 密集型应用,为了提高性能,将 I/O 操作重叠。线程可以同时等待不同的 I/O 操作
五、线程的缺点
• 等候使用共享资源时会使程序的运行速度变慢,这些共享资源主要是独占性的资源,如打印机等。
• 对线程进行管理要求额外的CPU开销。线程的使用会给系统带来上下文切换的额外负担,当这种负担超过一定程度时,多线程的缺点会表现得较突出,比如用独立的线程来更新数组内每个元素。
•线程的死锁。即较长时间的等待或资源竞争,以及死锁等多线程症状。
•对公有变量的同时读或写。当多个线程需要对公有变量进行写操作时,后一个线程往往会修改掉前一个线程存放的数据,从而使前一个线程的参数被修改;另外,当公用变量的读写操作是非原子性时,在不同的机器上,中断时间的不确定性会导致数据在一个线程内的操作产生错误,从而产生莫名其妙的错误,而这种错误是程序员无法预知的。
六、线程异常
-
线程异常与进程崩溃:
- 线程在运行过程中如果发生异常,如除零错误、访问野指针等,通常会导致线程崩溃。这些异常可能会引发操作系统层面的信号(例如
SIGFPE
对于除零错误,SIGSEGV
对于访问非法内存),这些信号会触发进程的终止机制。 - 当一个线程崩溃时,通常会导致整个进程的崩溃。这是因为线程共享进程的地址空间和资源,线程的异常可能会破坏进程的整体状态,从而导致进程无法继续安全地运行。
- 线程在运行过程中如果发生异常,如除零错误、访问野指针等,通常会导致线程崩溃。这些异常可能会引发操作系统层面的信号(例如
-
信号机制:
- 当线程发生致命异常时,操作系统会发送信号到进程,这些信号可能会导致进程的终止。例如,除零错误可能触发
SIGFPE
,访问非法内存可能触发SIGSEGV
。这些信号会通知操作系统对进程进行处理,通常会导致整个进程的退出。 - 在某些情况下,可以使用信号处理机制来捕获这些信号,从而进行一些清理工作或记录日志,但通常情况下,如果信号表示严重错误,进程会被终止。
- 当线程发生致命异常时,操作系统会发送信号到进程,这些信号可能会导致进程的终止。例如,除零错误可能触发