一篇围绕 Unix 线程基础展开的学习笔记。
在 进程基础 中,已介绍进程是资源分配与调度的基本单位。本篇聚焦线程的理论基础,不展开 pthread API:
- 线程的定义及其与进程的关系
- 并发与并行的区分及多线程的动机
- 用户线程与内核线程的层次与映射
- 经典线程模型(多对一、一对一、多对多)
- 隐式多线程与线程撤销
线程的概念
1. 基本定义
线程是进程中的一条执行流,也是 CPU 调度和执行的基本单位。
一个线程至少包含线程 ID、程序计数器、寄存器现场和线程栈。这些构成线程的控制流状态——描述当前执行位置、下一条指令和函数调用现场。
| 组成部分 | 作用 |
|---|---|
| 线程 ID | 线程的唯一标识 |
| 程序计数器 | 指出下一条要执行的指令 |
| 寄存器现场 | 保存当前计算状态 |
| 线程栈 | 保存函数调用过程中的局部状态 |
线程不是独立于进程的完整资源容器,而是进程的实际执行者,因此也常被称为”执行线程”。
2. 线程与进程的关系
进程与线程的归属关系如下:
| 对象 | 典型内容 |
|---|---|
| 进程 | 地址空间、代码段、数据段、堆、打开文件、信号处理方式 |
| 线程 | 程序计数器、寄存器集合、栈、调度状态 |
具体归属:
| 项目 | 归属 |
|---|---|
| 地址空间 | 进程共享 |
| 代码段 / 数据段 / 堆 | 进程共享 |
| 打开的文件描述符 | 进程共享 |
| 程序计数器 | 线程专有 |
| 寄存器现场 | 线程专有 |
| 线程栈 | 线程专有 |
| 调度状态 | 线程专有 |
进程上下文关注”这个运行实体拥有哪些资源”(地址空间、打开文件、信号处理方式),线程上下文关注”这条执行流当前执行到哪里”(程序计数器、寄存器、线程栈)。同进程内的线程切换只涉及线程上下文的替换;跨进程切换还需要切换进程上下文。
3. 单线程进程与多线程进程
| 类型 | 特点 |
|---|---|
| 单线程进程 | 进程内部只有一条执行流 |
| 多线程进程 | 进程内部有多条执行流,共享同一组进程资源 |
单线程进程结构简单,但同一时刻只能沿一条路径推进。多线程进程将界面响应、网络 I/O、后台计算等任务拆到不同执行流,现代浏览器、编辑器、服务器普遍采用这种结构。
线程的作用
1. 并发与并行
并发 concurrency 与并行 parallelism 是两个不同概念:
| 概念 | 含义 | 关键点 |
|---|---|---|
| 并发 | 多个任务在一段时间内都能取得进展 | 强调”都在推进” |
| 并行 | 多个任务在同一时刻真正同时执行 | 强调”绝对的同时执行” |
硬件层次上,一个物理 CPU 封装(处理器)可包含多个核,每个核可通过硬件多线程暴露多个逻辑处理器:
| 概念 | 含义 |
|---|---|
处理器 processor | 物理 CPU 封装,通常对应一个 CPU 插槽 |
核 core | 处理器内部可独立执行指令的计算单元 |
逻辑处理器 logical processor | 硬件多线程暴露给操作系统的执行上下文 |
操作系统看到的并行上限接近逻辑处理器数。在不同硬件结构下,并发/并行能力如下:
| 硬件结构 | 同时执行线程数 | 结果 |
|---|---|---|
| 单处理器、单核 | 1 | 仅能通过切换形成并发 |
| 单处理器、多核 | 最多约等于核心数 | 可并行 |
| 多处理器、多核 | 最多约等于全部核心数 | 并行能力更强 |
线程数增加不会带来无限线性加速。Amdahl 定律给出了上界:
其中 S 为程序中串行执行的比例,N 为可并行使用的处理单元数。若 S=0.25、N=4,理论加速比上限仅约 2.29——多核与多线程确实能提升性能,但收益始终受串行部分限制。
2. 多线程的动机与优势
| 场景 | 线程的作用 | 直接收益 |
|---|---|---|
| 交互式程序同时存在界面刷新、用户输入、后台任务 | 不同性质的工作拆到不同执行流 | 响应性 |
| 服务器持续处理大量请求 | 同一进程内并发处理多个连接或任务 | 经济性、资源共享 |
| 计算任务可拆分到多个处理单元 | 不同线程分担工作 | 可伸缩性 |
多线程的核心优势:
| 优点 | 含义 |
|---|---|
| 响应性 | 某个耗时任务阻塞时,其他线程仍可继续响应 |
| 资源共享 | 同一进程内线程天然共享地址空间和大部分资源 |
| 经济性 | 创建和切换线程通常比进程更便宜 |
| 可伸缩性 | 易于利用多核处理器并行执行任务 |
代价是竞争条件、同步复杂度和调试难度上升。
用户线程与内核线程
用户线程由应用程序或线程库在用户态组织,内核线程由内核调度器直接管理。两者不是并列的两类”线程品种”,而是处在不同层次——线程模型的核心问题就是二者的映射关系。
1. 层次关系
| 层次 | 对象 | 直接可见性 | 主要作用 |
|---|---|---|---|
| 用户态 | 用户线程 | 应用程序、线程库 | 组织程序内部执行流 |
| 内核态 | 内核线程 | 内核调度器 | 真正参与 CPU 调度 |
若系统仅提供用户态线程库,多个用户线程在 CPU 调度上可能被视为一个线程——内核未必知晓它们每一个的存在。
2. 用户级线程
| 方面 | 说明 |
|---|---|
| 管理位置 | 用户空间线程库负责创建、切换和调度 |
| 优点 | 不频繁陷入内核,创建和切换通常更快 |
| 局限 | 阻塞系统调用可能拖住整个用户态调度,难以利用多核 |
用户线程由线程库在用户态调度——线程库决定”下一个让哪个用户线程运行”,与内核调度器分配 CPU 是两个独立层次。当多个用户线程复用同一个内核线程时,若该内核线程因阻塞系统调用进入等待态,线程库得不到 CPU,无法切换到同进程中的其他用户线程。
3. 内核级线程
| 方面 | 说明 |
|---|---|
| 管理位置 | 线程的创建、销毁和调度由内核参与 |
| 优点 | 某线程阻塞时其他线程仍可运行,易于利用多核 |
| 代价 | 管理成本高于纯用户级线程 |
内核知晓每个线程的存在:某线程因系统调用阻塞时,内核可将同进程的其他线程调度到 CPU 上运行;多核机器上,不同内核线程可分布到不同处理核并行执行。Linux、Windows、macOS 均支持内核级线程。
4. 对比
核心区别在于调度权的归属。
| 对比项 | 用户级线程 | 内核级线程 |
|---|---|---|
| 调度者 | 用户态线程库 | 内核调度器 |
| 内核是否可见 | 不一定 | 是 |
| 阻塞影响 | 可能拖住整个用户态调度 | 通常只影响当前线程 |
| 多核利用 | 受限 | 容易并行 |
| 管理成本 | 通常更低 | 通常更高 |
线程模型
线程模型讨论用户线程与内核线程的映射关系。常见有 3 种。
1. 多对一模型
多个用户线程映射到一个内核线程。线程库在用户态本地切换,管理成本低;但从内核看,整个进程只对应一个可调度实体。若该内核线程因阻塞系统调用睡眠,所有用户线程均无法推进,且无法分配到多核并行执行。实现简单,但对现代多核系统不够友好。
2. 一对一模型
每个用户线程对应一个内核线程。一个线程阻塞时其他线程仍可运行,多核并行自然支持。代价:每创建一个用户线程即创建一个内核线程,线程数量多时内核管理成本上升。Linux 和 Windows 主要采用此模型。
3. 多对多模型
多个用户线程复用到多个内核线程上,试图结合前两者优点:用户态灵活管理大量线程,内核提供多个可调度执行实体。某个线程阻塞时,其他线程可复用别的内核线程继续执行。实现复杂度较高,因此现代主流系统更常见一对一模型。
三模型对比:
| 模型 | 映射关系 | 优点 | 局限 |
|---|---|---|---|
| 多对一 | 多用户线程→1 内核线程 | 用户态开销低 | 阻塞影响大,难利用多核 |
| 一对一 | 1 用户线程→1 内核线程 | 并发和并行能力强 | 内核线程多,成本高 |
| 多对多 | 多用户线程→多内核线程 | 灵活,兼顾并发与效率 | 实现复杂 |
4. 隐式多线程
隐式多线程将线程管理交给运行时系统:程序员描述任务,库或运行时决定如何创建、复用和调度线程。线程池是常见形式——预创建一组工作线程,从任务队列取任务执行,结束后归池复用,减少频繁创建/销毁开销并限制并发线程数。JVM 也是典型的隐式多线程代表。
| 方式 | 程序员关注 | 运行时负责 |
|---|---|---|
| 显式多线程 | 创建线程、回收线程、安排同步 | 较少 |
| 隐式多线程 | 提交任务、描述并行工作 | 线程创建、复用与调度策略 |
线程撤销
1. 基本概念
线程撤销 thread cancellation 指在线程正常完成之前提前终止它。例如,多线程并行搜索时,一个线程找到结果后其他线程即无继续运行的必要。被终止的线程称为目标线程 target thread。
目标线程的撤销方式:
| 方式 | 含义 |
|---|---|
| 异步撤销 | 发出请求后目标线程立即终止 |
| 延迟撤销 | 目标线程在安全位置检查请求,再有序结束自己 |
线程撤销伴随资源回收问题:线程可能正持有锁、修改共享数据或持有已分配资源,在不合适时机直接终止会导致程序状态不一致。
2. 异步撤销
异步撤销 asynchronous cancellation 发出请求后目标线程立即终止,不必等待配合。风险:
| 风险 | 说明 |
|---|---|
| 资源未释放 | 已申请的内存、文件、锁可能未正常清理 |
| 数据不一致 | 修改共享数据中途被打断,破坏一致性 |
| 行为难预测 | 线程断点不可控 |
3. 延迟撤销
延迟撤销 deferred cancellation 发出请求后,目标线程不立即终止,而是在撤销点 cancellation point 主动检查并有序退出——先完成必要的清理工作再结束自己。
| 对比项 | 异步撤销 | 延迟撤销 |
|---|---|---|
| 终止时机 | 立即 | 到达撤销点后 |
| 目标线程参与 | 基本不参与 | 主动检查并配合 |
| 资源清理 | 风险高 | 可控 |
| 程序状态 | 较差 | 更好 |
延迟撤销比异步撤销更安全、更常见。
小结
线程是进程内的执行流,也是 CPU 调度的基本单位。本篇的理论主线可以压缩如下:
| 主题 | 核心内容 |
|---|---|
| 线程与进程的关系 | 进程拥有资源,线程执行代码;同一进程内线程共享地址空间与大部分资源 |
| 并发与并行 | 并发强调多任务推进,并行强调同一时刻真正同时执行 |
| 用户线程/内核线程 | 用户线程由线程库管理,内核线程由调度器管理;调度权归属是关键区别 |
| 线程模型 | 多对一、一对一、多对多,决定用户线程如何映射到内核线程 |
| 隐式多线程 | 程序员提交任务,运行时负责线程创建与调度 |
| 线程撤销 | 异步撤销(立即终止)与延迟撤销(在撤销点有序退出) |
附注
1. JVM 隐式多线程
JVM 规范不规定 Java 线程如何映射到底层操作系统,由各 JVM 实现决定。Windows 采用一对一模式,每个 Java 线程映射到一个内核线程;Tru64 UNIX 等采用多对多模式。在实现上,Windows JVM 使用 Windows API 创建线程,Linux、Solaris、macOS 使用 Pthreads API。