线程的优缺点,与进程的关联和差异

一、首先确认一个结论

Linux 中的线程是用进程来模拟实现的,它在 Linux 中都叫做轻量级进程(LWP)。

  • 每个进程在自己的用户空间中拥有资源;进程内部的线程只要有自己独立的 PCB 和资源划分,就拥有了对应的资源
  • 本文的内容:缺点 → 线程出现异常的表现 → 用途 → 最后对比进程与线程

二、线程的优点

2.1 教材上的优点清单(7 条)

# 优点 一句话解释
① 创建一个新线程的代价要比创建一个进程小得多 进程已存在 = 资源已分配,线程只要建 PCB + 划分资源
② ★ 线程之间的切换,OS 要做的工作少很多 具体看下面的解释
③ 线程占用的资源要比进程少 线程拿的资源本身就是进程的一部分
④ 能充分利用多处理器的可并行数量 线程是调度的基本单位,多核才能跑满
⑤ 等待慢速 I/O 的同时,程序可以执行其他计算任务 多进程同样能做到,不是线程专有优点
⑥ 计算密集型应用,把计算分解到多线程 在多处理器上运行才划算
⑦ I/O 密集型 应用,让 I/O 操作重叠 同时等待不同的 I/O,提高吞吐

✖ 别把 ⑤ 当成线程的独有优点------进程同样也有这个优点

💡 如果在找工作面试时不用死背 7 条:你只需要记住"线程更轻量化"就够了,剩下的到时候现场展开。

2.2 为什么线程创建代价小(②③①)

操作 创建进程 创建线程
PCB / task_struct 要创建 要创建
地址空间(mm_struct) 要创建 不创建,共享
页表 要创建并构建映射 不创建,共享
运行时 还要不断处理缺页中断、调度、内存管理 直接参与调度即可

✔ 本质:创建线程时进程一定已经存在了------说人话就是资源已经分配了,线程只要"PCB + 资源划分",代价当然小。

2.3 两种应用类型:计算密集型 vs I/O 密集型

在 Linux 里 一切皆进程(你在系统中跑的所有任务全以进程身份运行),而计算任务可以分成两类:

类型 典型场景 特征 多线程策略
计算密集型 加密解密、压缩解压、各种算法、游戏画面、大模型训练 大部分时间消耗 CPU 资源 线程数 ≈ CPU 核数,不要多
I/O 密集型 写磁盘、网络下载/上传、访问数据库 大部分时间在等 线程可以多创建一点,让 I/O 重叠

计算密集型的例子(学校和 AI 都能套上)

  • 大文件排序 :1TB 数据切成 4 份(每份 256G 甚至 1G),创建 4 个线程各排各的,局部有序后再归并成一个更大的有序序列
  • 游戏画面 :显示器本质是一个 XY 二维平面 ,上面有无数像素点,每个像素点由 RGB 三元素 构成;画面刷新 = 高频更新每个坐标的 RGB 数值(等价于三个二维矩阵,也就是一个三维矩阵),所以它是典型的矩阵式计算
  • 大模型 :数据矩阵化 + 前向传播 / 反向传播也是矩阵更新 → 和游戏画面的矩阵计算非常像
  • 所以大模型训练更爱用 GPU :GPU 本来就能把大型游戏的计算扛住,矩阵计算面前 CPU 就是个小学生

I/O 密集型的例子(多线程下载)

复制代码
单线程下载:带宽很宽,但远端不一定准备好 → 大部分时间在白等
多线程下载:线程①下载第 1GB、线程②下载第 2~3GB、线程③......各下同一文件的不同区域
           → 把带宽压力拉满;万一卡住了,四个线程同时在等,总有人先读到数据

2.4 线程不是越多越好(重要结论)

✖ 误区:听说多线程好,那就开十个八个线程。

场景 合理线程数 为什么
计算密集型 = CPU 核数(正相关) 2 个 CPU 开 8~10 个线程,每个核平均分到 4~5 个线程,它除了算还得切换------本来用于计算的算力被切换吃掉了,反而更慢
单核上的计算密集型 单线程最快 CPU 上永远只有你一个人在算,没人打扰,没有切换成本
I/O 密集型 可以多一些 大部分时间在等,线程多了,概率上总有那么几个线程能读到数据,提升上传/下载效率

💡 下次有人问"线程数怎么定",答这句:看任务是吃 CPU 还是在等 I/O。


三、★ 重点:线程切换为什么比进程切换更便宜

3.1 先看清:进程切换到底在切什么

进程切换的核心就三件事:

  1. 切换 PCB / task_struct(背后就是一大堆数据结构:地址空间、信号、文件......全跟着换)
  2. 切换 CR3 寄存器 ------CR3 里放的是页目录起始地址 ,所以 CR3 一换,页表和整套虚拟↔物理映射关系就全换了
  3. 保存并恢复 CPU 寄存器上下文(保存到 PCB 里,再把下一个进程的上下文装上)

💡 OS 怎么知道"当前是谁在跑"?内核里有一个当前进程指针 current (更准确说是 per-CPU 的),它永远指向当前正在运行的 task_struct;调度器按(大 O(1) 调度算法)优先级从 active 队列里选中一个进程后,current 就指向它。

  • 内核源码里 current 的定义前面加了 register 关键字------这是个建议型关键字,意思是"建议编译器把这个指针放到寄存器里",因为取当前进程这个操作太频繁了
  • 所以:PCB 也好、CR3 页目录也好,本质上都是 CPU 内部的寄存器 → 进程切换 = 把 CPU 的硬件上下文 换一遍,完事

3.2 线程切换:省掉了什么(但还不够)

判定规则很简单:

OS 判断"下一个线程还在不在当前进程里"------看两者指向的地址空间是不是同一个。

c 复制代码
if (next->mm == current->mm)   // 地址空间相同 = 同一个进程内的线程
    /* 不用换 CR3、不用换页表 */;
else                            // 换到别的进程的线程 → 这已经是进程切换了
    switch_mm();                // 换 CR3 + 页表

线程切换时:

  • ✔ task_struct 要切(每个线程有自己独立的 PCB)
  • ✔ CPU 寄存器上下文要保存/恢复(运行时有自己的临时数据)
  • ✖ 页表不用换、地址空间不用换、CR3 不用保存 ------ 因为所有 PCB 都指向同一个 mm_struct,映射关系天然不变

⚠️ 但这并不是降低成本的根本原因:

"好像能低那么一点点,但总有点杯水车薪------一个寄存器的值而已,保存一下也无伤大雅。单看这个,根本看不出来谁的成本明显更低。"

所以 3.2 不是主要原因,真正的原因在下面。

3.3 真正的元凶:Cache 和 TLB 失效

CPU 内部针对当前进程 ,其实维护着两大缓存:

缓存 缓存的东西 位置
Cache(硬件高速缓存) 用户数据本身(内存里的代码/数据) CPU 内部,一般还分 L1/L2/L3 多级
TLB(快表 / 转译后备缓冲器) 虚拟地址 → 物理地址 的映射关系 CPU 内部,与 MMU 协同

① Cache:对用户数据做缓存(对用户透明,你感知不到)

复制代码
你写代码:int a=10, b=20, c=30, d=100;
读 a 时,CPU 不会只把 a 读进来 ------ 它依据【局部性原理 / 预加载策略】,
会把 a 周边的一整块数据都搬进 CPU 内部的 Cache。
→ 第一次访问:穿透到内存
→ 第二次访问:命中 Cache,直接从 CPU 内部拿,不用再跑内存
→ 没命中:仍然要做内存块与 Cache 之间的置换

② TLB:缓存虚拟到物理的映射关系(复习上一节)

  • 多级页表省了空间,代价是查询要多次访存;于是 CPU 里加了一层 TLB:MMU 先查 TLB,命中就直接拿到物理地址,未命中才走多级页表这个"保底武器",查完顺手写回 TLB

③ 关键结论

场景 Cache TLB 后果
进程切换 直接失效 直接失效 下次回来还要做热加载 ,把数据、映射项重新"热"起来------这才是最大的成本
线程切换 不会大面积失效 永远都不会大面积失效 不需要重新热起来 → 成本极低

✔ 为什么线程切换不会失效?因为它压根不换页表。 映射关系的条目是稳定的、不变的,TLB 里缓存的虚拟→物理映射自然不用动。

一句话:进程切换会让 TLB 和 Cache 全部失效,下次运行需要重新缓存;线程切换不会导致缓存失效 ------ 这才是线程切换成本低的根本原因。

💡 这跟软件没关系,是硬件层面的事;但它和"你到底选多进程还是多线程"密切相关。


四、线程的缺点

缺点 说的是什么 注脚
性能损失 线程创建得过多 → 切换成本变高 → 反而变慢 严格说是"创建太多"的缺点,多进程也一样
健壮性降低 一个线程出错,会连累整个进程被干掉 因为共享地址空间
缺乏访问控制 线程之间没有天然隔离,一个线程可以直接改掉另一个线程的数据 进程则有天然独立性
编程难度提高 要处理共享数据的并发访问 后续线程同步互斥要解决的核心问题

4.1 我对这份"缺点清单"的态度

✖ 不要死记这些缺点,它们描述的其实是**"用不好"的后果**:

  • 什么性能损失?------ 你别创建那么多不就完了
  • 什么健壮性降低、缺乏访问控制?------ 你别去访问别人的东西不就完了
  • 什么编程难度高?------ 你把代码写好一点就没问题

✔ 真正想表达的差异只有一句:进程天然具有独立性 (你爱怎么写怎么写,不会影响别的进程);线程一旦出问题,就会影响同进程内的其他线程。

4.2 辩证的一面:缺乏访问控制也是优点

💡 在这个世界上,所有的事情都只有特点;优缺点是人为强加的,任何一件事都有两面性。

回想进程间通信那一段:折腾半天(管道、共享内存、消息队列......),研究的本质都是"怎么让不同进程看到同一份资源"。

  • 而线程天然就能看到 同一份资源 → 在线程里做资源共享,是特别容易的事情
  • 所以"缺乏访问控制"既是缺点,也正是多线程的最大优点

五、线程出现异常的表现

5.1 结论

线程是进程的执行分支,线程出异常 = 进程出异常。

一旦某个线程出现 除零、野指针(越界访问非法地址) 等错误:

  1. 该线程崩溃
  2. OS 依据信号机制给整个进程 发信号,终止进程
  3. 进程没了,它赖以生存的所有资源------地址空间、页表------全部被释放
  4. 进程内的所有线程自然也就没有存在下去的必要了

💡 成语总结:覆巢之下,安有完卵(皮之不存,毛将焉附)。

5.2 线程的用途(加餐)

  • 合理使用多线程,可以提高 CPU 密集型程序的执行效率(多核算力跑满)
  • 可以提高 I/O 密集型程序的用户体验(等待 I/O 的同时还能响应交互)

六、进程 vs 线程:到底哪些共享、哪些私有

6.1 一句话定位

定位 资源
进程 资源分配的基本单位 大部分资源独占,少量共享(如进程间通信)
线程 调度的基本单位 大部分资源共享 ,少量私有

6.2 ★ 私有(面试必答的两条,答对就 80 分)

面试官问"线程相比进程,哪些资源是私有/独占的",必须答出这两条:

私有资源 说明 它证明的事
① 一组寄存器(线程的上下文数据) 运行时有各种临时数据,CPU 内绝大部分寄存器的值都是线程私有的,要做上下文保存/恢复 ✔ 线程是可以被独立调度的
② 独立的栈结构 函数调用要形成栈帧,栈里保存的是临时变量 ✔ 线程是一个动态的概念(有生命周期,压栈/出栈完成临时数据保存)

为什么栈必须独立?------ 如果多个线程共用一个栈:A 线程入栈,B 线程说我要出栈,栈结构直接就烂完了。

6.3 共享(除了上面那些,剩下的全共享)

共享资源 说明
代码区、数据区 共享地址空间 → 一个全局变量被两个线程访问时,访问的是同一个变量;A 线程能调的函数,B 线程也能调
文件描述符表 每个进程有自己的 fd 表(讲管道时强调过),线程各自的 PCB 都指向同一张进程 fd 表
每种信号的处理方式 进程里设置了忽略/捕捉,之后创建的线程对信号的处理动作是一致的
当前工作目录(cwd / pwd) 进程在哪个目录,线程就在哪个目录
用户 ID 和组 ID 进程"是谁在跑"的身份
堆、共享库、3-4GB 内核区、页表 本质都是同一份地址空间

6.4 四种组合形态

形态 描述
单线程进程 以前学 C/C++、数据结构时写的全是这种
多进程(每个进程单线程) 之前讲的多进程阶段
单进程多线程 一个进程内可以有多个线程
多进程 + 多线程 更复杂的情况,需要时出现

✔ 所以换个角度看待从前学过的进程:以前的单进程,就是一个只具有一个线程执行流的进程。


七、总结

7.1 核心结论串

  1. Linux 线程 = 用进程模拟的轻量级进程(LWP) ,共享同一份 mm_struct 和页表
  2. 线程优点记得一句就够:线程更轻量化;展开则是创建代价小、资源占用少、吃满多核、I/O 可重叠
  3. 线程数不是越多越好:计算密集型 ≈ CPU 核数,I/O 密集型可多开
  4. ★ 线程切换更便宜的根本原因:不换页表 ⇒ TLB 和 Cache 不会大面积失效;而"少保存一个 CR3"只是表象,杯水车薪
  5. 线程缺点(性能损失/健壮性降低/缺乏访问控制/编程难度高)本质是"用不好"的后果;而"缺乏访问控制"的另一面正是线程共享资源极其容易
  6. 线程异常 = 进程异常 → 信号终止进程 → 线程陪葬(覆巢之下安有完卵)
  7. 进程 = 资源分配基本单位,线程 = 调度基本单位;线程共享代码/数据/fd 表/信号处理/cwd/uid,私有只有上下文寄存器 + 独立栈(外加 tid/errno/信号屏蔽字/优先级)

7.2 进程 vs 线程 速查表

对比项 进程 线程
定位 资源分配的基本单位 CPU 调度的基本单位
地址空间 独立 mm_struct + 独立页表 共享
资源 大部分独占 大部分共享,少量私有(寄存器上下文 + 独立栈)
创建代价 大(PCB + 地址空间 + 页表 + 映射 + 缺页中断) 小(PCB + 资源划分)
切换代价 大(换 CR3/页表,TLB 与 Cache 全部失效,需要重新热加载) 小(不换页表,缓存不失效)
健壮性 天然独立,互不影响 一个线程崩溃 → 整个进程被终止
共享资源的难度 难(IPC:管道/共享内存/消息队列...) 极易(天然看到同一份资源)
Linux 实现 task_struct + 独立地址空间 task_struct + 共享地址空间(LWP)
相关推荐
曹牧1 小时前
Spring MVC:@RequestMapping
java·spring·mvc
不会就选b1 小时前
Linux之http会话
服务器·网络协议·http
江屿风1 小时前
【Linux系统】【从【收尾】缓冲区到【新开】磁盘块:一节课打通文件系统底层原理】流食般投喂
linux·运维·服务器·人工智能·笔记
李游Leo1 小时前
HarmonyOS 7 + ArkUI + Adaptive Layout 学习笔记:折叠屏多形态布局适配与窗口状态响应机制【鸿蒙心迹】
笔记·学习·harmonyos
_upupup1 小时前
Linux中的权限解析
linux·服务器
曹牧1 小时前
Spring MVC : Controller 层URL划分
java·运维·服务器·前端
路漫漫其修远兮sjw1 小时前
Linux 运维知识点笔记(中级)
linux·运维·笔记
卓怡学长2 小时前
w203基于springboot钢材仓库管理系统的设计与实现
java·spring boot·spring·intellij-idea
sunburn-2 小时前
Java 排序算法详细教学:从冒泡排序到快速排序
java·开发语言·数据结构·ide·算法