5649 字
28 分钟
2.1进程和线程简介2

2.1.7 线程和多线程模型#

1. 线程的基本概念#

引入进程的目的是使多道程序能够并发执行,从而提高资源利用率和系统吞吐量;而引入线程(Thread)则是为了进一步降低程序并发执行时的时空开销,提升操作系统的并发性能。

线程可被通俗地理解为“轻量级进程”,它是程序执行流的最小单元,也是处理器调度的基本单位。在多线程操作系统中,进程不再作为基本的执行实体,但仍保留与执行相关的状态。所谓进程处于“执行”状态,实际上是指该进程中的某个线程正在运行。线程的主要属性如下。

(1) 轻量级执行实体#

线程本身不拥有独立的系统资源(如地址空间、打开的文件等),但拥有运行所必需的私有数据结构,包括线程ID程序计数器寄存器集合。每个线程都具有唯一的标识符和一个线程控制块(TCB),用于记录其执行上下文(如寄存器状态、栈指针等)。

(2) 共享代码段#

同一进程中的多个线程可并发执行相同的程序代码。例如,当多个用户同时访问一个Web服务器时,服务器进程可创建多个线程,各自服务不同的用户请求,显著提升并发能力。

(3) 资源共享#

同一进程内的所有线程共享该进程的代码段、数据段、堆、文件描述符等系统资源。这种机制使得线程间的通信和数据共享非常高效,无须复杂的进程间通信(IPC)机制。

(4) 独立调度单位#

线程是CPU调度和执行的基本单位。在单CPU系统中,多个线程通过时间片轮转等方式轮流使用CPU;在多CPU系统中,不同线程可并行运行于多个CPU核心上。若多个核心同时执行同一进程的不同线程,则能显著缩短该进程的整体处理时间。

(5) 生命周期#

线程从创建开始,经历就绪、运行、阻塞等状态变化,直至最终终止。

引入线程后,进程的内涵发生了重要变化:进程成为除CPU外的系统资源分配单位,而线程则成为CPU调度和执行的基本单位。当线程切换发生在同一进程内部时,由于地址空间等资源无须切换,其开销远小于进程切换。接下来,将从多个维度对线程与进程进行比较。

2. 线程与进程的比较#

  1. 调度。在传统的操作系统中,进程既是资源拥有者,也是独立调度的基本单位,每次调度都需要进行完整的上下文切换,开销较大。引入线程后,线程成为独立调度的基本单位。在同一进程中,线程切换不会引起进程切换;但若从一个进程的线程切换到另一个进程的线程,则仍会触发进程切换。
  2. 并发性。在支持线程的操作系统中,并发性得到显著增强:不仅进程之间可以并发执行,同一进程内的多个线程也可以并发执行,甚至不同进程中的线程也能并发执行。这种多层次的并发机制有效提升了系统资源利用率和吞吐量。
  3. 拥有资源进程是系统资源分配的基本单位;而线程不拥有独立的系统资源,但拥有运行所必需的私有数据结构。同一进程的所有线程共享该进程的地址空间及资源。
  4. 独立性。每个进程拥有完全独立的地址空间,默认情况下,其他进程无法直接访问其任何内存区域。相应的,一个进程中的线程对其他进程完全不可见。同一进程内的线程则因共享地址空间而紧密协作,旨在提升并发性能。
  5. 系统开销。创建或撤销进程时,系统需分配或回收PCB、地址空间、I/O资源等,开销显著;而创建或撤销线程仅需分配栈和少量内核对象,开销很小。类似地,进程切换涉及整个地址空间和资源上下文的切换,而线程切换只需保存和恢复寄存器状态。此外,由于同一进程内的线程共享地址空间,它们之间的同步与通信非常容易实现。
  6. 支持多处理器系统。对于单线程进程,尽管可以在不同时间被调度到任意CPU上运行,但在任一时刻只能在一个CPU上执行。而对于多线程进程,系统可将其中的多个线程同时调度到多个CPU上并行执行,从而真正发挥多核处理器的性能优势。

3. 线程的状态与生命周期#

由于线程之间共享资源,并存在同步与互斥等制约关系,其执行过程具有明显的间断性。相应地,线程在其生命周期中会经历以下三种基本状态

  • 运行态:线程已获得CPU,正在执行。
  • 就绪态:线程已具备所有运行条件,只需获得CPU即可立即执行。
  • 阻塞态:线程因等待某一事件(如I/O完成、锁释放等)而暂时无法继续执行。

线程这三种基本状态之间的转换与进程基本状态之间的转换是类似的。

线程具有完整的生命周期:由创建而产生,经调度而执行,最终因终止而消亡。为支持这过程,操作系统提供了相应的系统调用或库函数。

用户程序启动时,通常仅有一个初始线程在执行,其主要任务是创建其他线程。创建新线程时,需调用线程创建函数,并传入必要的参数,例如:指向线程主函数的入口地址、堆栈大小、调度优先级等。创建成功后,系统返回唯一的线程标识符,供后续操作使用。

操作系统的调度器根据线程的优先级和其他调度策略决定哪个线程获得CPU使用权。调度机制可选用多种方式。当线程被调度到CPU上时,它进入运行态,开始执行其主函数中的代码。在此期间,线程可能会与其他线程进行同步或互斥操作,以确保数据一致性。

线程终止的原因可能有多种。例如,线程正常执行完其主函数;在运行过程中发生未处理的异常而被迫退出;主动调用线程终止函数;或被其他线程通过取消机制强制终止。在大多数操作系统中,线程终止后并不会立即释放其所占用的资源。只有当进程中的其他线程执行了相应的分离操作后,被终止的线程才会与资源分离,其资源才能被系统回收并供其他线程使用。

已被终止但尚未释放资源的线程仍可被其他线程调用,以使其重新恢复运行。

4. 线程控制块#

与进程类似,操作系统为每个线程维护一个线程控制块(Thread Control Block, TCB),用于记录和管理线程的运行信息。TCB通常包含以下内容:

  1. 线程标识符(ThreadID);
  2. 寄存器集合,包括程序计数器、状态寄存器和通用寄存器;
  3. 线程当前状态,用于描述线程正处于何种状态;
  4. 调度优先级
  5. 线程局部存储区,用于保存线程私有的数据;
  6. 堆栈指针,指向该线程的私有栈,用于过程调用时保存局部变量、返回地址等。

同一进程中的所有线程共享该进程的地址空间和全局变量。每个线程拥有独立的私有栈,这些栈位于进程的地址空间内,因此从技术上讲,一个线程可以访问另一个线程的栈内容。然而,这种做法严重违反编程规范,会破坏线程封装性和程序正确性,实际开发中应严格避免。

5. 线程的实现方式#

线程的实现可以分为两类:用户级线程(User-Level Thread, ULT)和内核级线程(Kernel-Level Thread, KLT)。其中,内核级线程也称为内核支持的线程

(1) 用户级线程(ULT)#

通俗地说,用户级线程是指“从用户视角看到的线程”。在该模型中,所有线程管理工作(包括创建、切换、撤销等)均由应用程序在用户空间(用户态)完成,无须操作系统介入。正因如此,操作系统内核完全感知不到这些线程的存在。应用程序只需引入线程库,即可被开发为多线程程序。通常,用户程序启动时仅包含一个初始线程;在执行过程中,该线程可在任意时刻调用线程库提供的创建函数,在同一进程中派生出新的线程。

对于采用用户级线程的系统,调度仍以进程为单位进行。调度器只能感知到进程,无法察觉进程内部的多个用户级线程,因此它仅为整个进程分配一个时间片。例如,假设系统采用时间片轮转调度,每个进程每次获得100ms的CPU时间。若进程A仅包含1个用户级线程,则当该进程获得时间片时,其唯一的线程可独占全部100ms的CPU时间;而若进程B包含100个用户级线程,则这100ms必须在其内部的100个线程之间分配。倘若进程B内部采用简单的轮转调度,那么每个线程平均仅能获得1ms的CPU时间。由此可见,进程A中的线程所获得的CPU时间是进程B中各线程的100倍。这种差异导致在多线程密集型任务中,不同进程间的线程实际执行效率极不均衡,从而在系统层面体现出明显的调度不公平性。

这种实现方式的优点:

  1. 线程切换在用户空间完成,无须切换到内核态,避免了模式切换的开销。
  2. 调度策略可由应用程序定制,不同进程可根据自身需求采用不同的线程调度算法。
  3. 用户级线程的实现与操作系统无关,线程管理代码属于用户程序的一部分,具有良好的可移植性。

其缺点在于:

  1. 系统调用的阻塞问题,当某个用户级线程执行一个阻塞型系统调用时,整个进程会被内核挂起,导致进程中的所有线程均被阻塞。
  2. 无法发挥多处理器的并行优势,由于内核仅将该进程调度到一个CPU上执行,即使系统配备了多个CPU核心,同一进程中的多个用户级线程也无法同时在不同CPU上并行运行,只能在单个CPU上交替执行。
(2) 内核级线程(KLT)#

在操作系统中,无论是系统进程还是用户进程,都依赖内核的支持运行。内核级线程的管理完全由操作系统内核负责,所有线程相关的操作均在内核空间(内核态)完成。操作系统为每个内核级线程维护一个线程控制块(TCB),内核通过该TCB感知线程的存在并对其进行控制。

这种实现方式的优点:

  1. 能发挥多处理器的并行优势,内核可将同一进程中的多个线程同时调度到不同CPU核心上并行执行。
  2. 阻塞处理更灵活,若进程中的某个线程因系统调用被阻塞,内核仍可调度该进程中的其他线程运行,或切换到其他进程的线程继续执行。
  3. 内核自身可采用多线程技术,从而提升系统服务的并发性和整体效率。

其缺点在于:线程切换需从用户态陷入内核态。由于线程在用户态运行,而调度由内核完成,每次切换都涉及模式转换和上下文保存/恢复,系统开销较大。

(3) 组合方式#

某些系统采用组合方式(混合模型)实现多线程。在该模型中,内核支持多个内核级线程的创建与调度,同时允许用户程序在用户空间管理多个用户级线程。这种方式能结合两种模型的优点,并克服各自的不足:多个线程可在多CPU上并行执行;单个线程阻塞不会导致整个进程挂起;用户级线程切换可在用户空间快速完成,减少内核介入。根据用户级线程与内核级线程之间的映射关系不同,形成了以下三种典型的多线程模型:

  1. 多对一模型。将多个用户级线程映射到一个内核级线程。线程的调度和管理在用户空间完成。每个进程仅拥有一个内核级线程,仅当用户线程需要访问内核时,才将其映射到一个内核级线程上,但每次只允许一个线程进行映射。

    优点:线程管理在用户空间进行,无须切换到内核态,因此线程切换的效率较高。

    缺点:当任一用户级线程执行阻塞型系统调用时,整个进程都会被内核挂起;在任何时刻最多只能有一个线程与内核交互,无法在多处理器系统中实现并行执行。

  2. 一对一模型。将每个用户级线程映射到一个独立的内核级线程。进程中的用户级线程与内核级线程一一对应,线程的调度由内核完成,需要切换到内核态。

    优点:当某个线程被阻塞时,内核可调度同一进程中的其他线程运行,支持真正的并发执行,且可在多CPU系统上并行运行。

    缺点:每创建一个用户线程,系统就必须创建一个对应的内核线程,导致较大的系统开销(如内核TCB占用内存、调度负担增加),限制了系统可支持的最大线程数。

  3. 多对多模型。将 nn 个用户级线程映射到 mm 个内核级线程,要求 nmn \ge m

    用户级线程由应用程序在用户空间调度,而内核级线程由内核调度到多个CPU上执行。特点:既克服了多对一模型并发度低的缺陷,又避免了一对一模型资源开销过大的问题。同时兼具两者优点:用户级线程切换高效,且支持多核并行与阻塞隔离。

(4) 线程库(thread library)的实现#

程序员通常通过线程库提供的API来创建和管理线程。线程库的实现主要有两种方式:

  1. 纯用户空间实现:线程库完全位于用户空间,所有代码和数据结构均在用户态。调用库函数仅触发本地过程调用,无须陷入内核。此类实现对应用户级线程模型。
  2. 内核支持实现:线程库的底层依赖操作系统内核。库中的API调用通常会触发系统调用,由内核完成实际的线程操作。此类实现对应内核级线程模型。

目前广泛使用的三种主流线程库包括:POSIX Pthreads、Windows 线程API和 Java 线程API。Pthreads 是 POSIX 标准定义的线程API,其具体实现可基于用户级或内核级线程。Windows 线程API 直接构建于 Windows 内核之上,属于内核级线程实现。Java 线程 API允许在 Java 程序中直接创建线程。由于JVM运行于宿主操作系统之上,Java线程通常通过调用宿主系统的线程库实现,因此在 Windows 上使用 Windows API,在类UNIX系统上则使用 Pthreads。

2.1.8 本节小结#

本节开头提出的问题的参考答案如下。

  1. 为什么要引入进程?

    在多道程序环境下,多个程序并发执行并共享系统资源,导致运行过程中相互制约,表现出间断性、异步性和不可预测性。然而,传统的“程序”仅是一组静态的指令集合,无法反映其在内存中的动态执行行为。例如,无法体现程序何时开始、何时暂停、如何与其他程序交互等。为了准确刻画程序并发执行的动态特性,并为操作系统提供有效的管理机制,进程这一概念被引入。进程是程序并发执行的基本单位,也是操作系统进行资源分配和调度的实体。

  2. 什么是进程?进程由什么组成?

    进程是一个具有独立功能的程序在其特定数据集上的一次执行过程。它是一个动态的、活动的实体,不仅包含程序代码,还包含当前的执行状态(如程序计数器的值、处理器寄存器的内容等)。一个进程实体由程序段、相关数据段和PCB三部分构成。其中,PCB是进程存在的唯一标志,操作系统通过PCB感知并控制进程的整个生命周期。

  3. 引入进程主要解决了多道程序并发执行中的哪些关键问题?

    引入进程有效解决了多道程序并发执行带来的三大问题:

    1. 失去封闭性:在缺乏进程机制的环境中,程序执行易受其他程序干扰(如共享内存被修改),导致行为不可控;进程通过地址空间隔离,为每个程序提供独立、受保护的执行环境,从而恢复一定程度的封闭性。
    2. 执行过程的间断性:程序可能因I/O请求、时间片用尽或调度决策而被中断;进程利用PCB保存和恢复执行上下文,确保中断后能准确、连续地继续执行。
    3. 结果不可再现性:在并发环境下,相同输入可能因执行顺序不确定而产生不同输出;进程的隔离机制与操作系统的调度策略共同保障了执行的可控性与结果的可再现性。通过为每个程序建立独立的进程实体,操作系统得以高效实现调度、切换、资源分配与保护,显著提升系统资源利用率和并发处理能力。

本节主要围绕“什么是进程”展开阐述,旨在为后续内容奠定概念基础。然而,对于尚未接触过操作系统相关知识的读者而言,“进程”作为一个抽象的动态实体,可能仍显得模糊不清。为了帮助大家更直观地把握其本质,我们引入一个贴近生活的类比。

我们可以将进程类比为“人的生命历程”。首先,人的生命历程是一个动态的、过程性的存在,而非静态的描述;同样,进程也不是一段固定的程序代码,而是一次正在执行的活动。要研究生命历程,必须先明确其主体“人”;对应地,进程的主体是进程映像,它由程序段、数据段和PCB组成。其中,PCB就如同人的“身份标识”,记录了该进程的唯一身份与当前状态。人在一生中会经历多种状态:出生、成长、奋斗、受挫、康复、衰老乃至离世;类似地,进程在其生命周期中也会在创建、就绪、运行、阻塞、终止等状态之间转换。例如,一个人从“充满斗志”(就绪)进入“发奋图强”(运行);若遭遇挫折,可能陷入“失落”(阻塞);在他人鼓励下,又可重新“充满斗志”(回到就绪)。这正对应于进程从就绪态转换为运行态,因等待事件而进入阻塞态,待事件发生后再次返回就绪态的典型状态迁移。通过“人生历程”这一过程性视角来理解进程,有助于我们超越对“程序=进程”的误解,真正把握进程作为动态执行实体的核心特征。当然,任何类比都有其局限性。上述比喻虽有助于建立直觉,但不能替代严谨的技术定义。因此,建议读者在借助类比形成初步理解后,回过头来重新阅读本节前文的正式阐述。这种“感性认知→理性回归”的学习路径,往往能带来更深刻、更准确的理解。

这里再给出一些学习计算机科学的建议。许多同学在学习过程中容易陷入一个误区:过于关注定理和公式的应用,而忽视了对基础概念的理解。这一倾向往往源于长期应试训练,毕竟熟练套用公式和定理在考试中见效快、收益明显。然而,公式和定理的应用固然重要,唯有深入理解基础概念,才能真正透彻地掌握一门学科,进而激发兴趣,培养创造性思维。

评论