《操作系统导论》第 26 章:并发:介绍 - 深度知识架构
1. 核心矛盾 (The Crucial Problem)
多个并发执行的线程共享同一内存地址空间,但底层的时钟中断和操作系统调度是不可控的,这导致对共享数据的访问随时可能被打断,从而产生不确定(错误)的计算结果。
2. 核心概念 (Core Concepts)
- 线程 (Thread):
- 定义:为单个运行进程提供的新抽象。一个多线程 (Multi-threaded) 程序会有多个执行点(多个程序计数器,Program Counter, PC),每个线程都有自己的独立栈 (Stack) 用于存放局部变量,但它们共享同一个地址空间。
- 角色:并发执行的基本载体,它是程序内部并发能力的来源。
- 临界区 (Critical Section):
- 定义:访问共享资源(通常是共享变量或数据结构)的一段代码。
- 角色:并发程序中的“危险地带”。多线程同时执行这段代码是引发错误的根源。
- 竞态条件 (Race Condition):
- 定义:多个执行线程大致同时进入临界区,试图更新共享数据结构,导致结果取决于代码执行的时间顺序的现象。
- 角色:并发程序运行时表现出的“病症”,它是我们需要极力避免的错误状态。
- 不确定性 (Indeterminate):
- 定义:由于竞态条件的存在,程序的输出因运行而异,结果不再是确定的 (Deterministic)。
- 角色:并发计算带来的直接恶果,破坏了计算机系统“给定相同输入必产生相同输出”的基本预期。
- 互斥 (Mutual Exclusion):
- 定义:一种保证如果一个线程在临界区内执行,其他线程将被阻止进入临界区的属性或机制。
- 角色:治疗竞态条件的“唯一解药”,它将并发的执行流在关键节点强制串行化。
- 原子性 (Atomicity):
- 定义:将一系列指令动作作为一个不可分割的单元执行(全部发生或全不发生),在执行中间不能被中断。
- 角色:并发控制的“终极愿望”。
3. 逻辑演进 (Logical Evolution)
为了解决共享数据更新的问题,系统经历了如下的逻辑推演:
- 最初的直觉方案(直接操作):既然多个线程共享内存,那直接在高级语言(如 C 语言)中写下
counter = counter + 1这样的代码即可。 - 遇到的致命问题(不可控的调度):这行看似简单的高级代码,在底层汇编层面会被拆分为 3 条独立的指令:1) 从内存加载 (Load) 到寄存器;2) 寄存器值加一 (Add);3) 从寄存器保存 (Store) 回内存。
- 由于操作系统 (Operating System, OS) 的时钟中断随时可能发生,如果线程 A 在执行完 Load 和 Add 后(还未 Store)被中断,OS 将 CPU 切换给线程 B。线程 B 此时读取的是内存中的旧值,更新后写入。随后线程 A 恢复运行,再把自己刚才计算的值覆盖进去。
- 这就产生了一个经典的竞态条件,导致其中一次更新操作凭空“丢失”了。
- 理想化的成熟方案(超级硬件指令):我们真切地希望硬件能提供一种类似于
memory-add的“超级指令”,单步就能无中断地完成加载、增加、保存的全部工作。 - 现实中的成熟方案(同步原语):由于硬件不可能为所有复杂的数据结构(如 B 树)提供专属的超级指令,最终的解决方案是退而求其次:要求硬件提供少量基础的“原子指令”。操作系统基于这些硬件支持,构建出通用的同步原语 (Synchronization Primitive)(比如锁和条件变量)。程序员在临界区前后显式调用这些原语,通过互斥来人为实现代码块的“原子性”。
4. 机制与策略 (Mechanisms vs. Policies)
- 底层的“实现手段”(机制 - Mechanisms):
- 中断机制与上下文切换:原本为了实现 CPU 虚拟化而发明的机制,在共享数据场景下反而成了制造麻烦的“罪魁祸首”。
- 硬件原子指令:为了解决上述麻烦,底层硬件必须提供一些无法被中断打断的特殊指令支持(未来的章节会深入讨论)。
- 上层的“决策逻辑”(策略 - Policies):
- 调度策略:操作系统的调度程序决定了哪个线程何时运行、何时被中断。程序员在编写并发代码时,必须将其视为“充满恶意的”,即永远不要假设调度策略会以有利于你的顺序执行。并发代码必须在任何最糟糕的调度策略下都能保证绝对正确。
5. 设计折衷 (Design Trade-offs)
- 牺牲“极致的并发度与性能”,换取“数据的正确性与确定性”:本来引入多线程是为了让程序在多处理器上并行跑得更快。但在临界区,为了避免竞态条件,我们必须引入互斥原语。这意味着在通过临界区时,原本可以并行的线程被迫排队“串行执行”。增加同步原语不仅引入了获取和释放锁的额外开销,还限制了程序的并发上限,这是为了保证计算正确而必须付出的代价。
6. 关键洞察 (Key Insights)
- 操作系统是世界上第一个并发程序:为什么要在操作系统课里学并发?因为 OS 自身就是第一个面临并发挑战的程序。不管是系统调用还是外设中断,它们都随时可能触发,并与当前正在执行的内核代码交错。内核的数据结构(如进程列表、页表)本质上就是巨大的共享变量,必须被小心地保护起来。
- 原子操作是构建可靠系统的基石:“全部发生或全不发生 (All or Nothing)”不仅适用于处理多线程更新共享变量(并发),它在文件系统防止磁盘故障崩溃(持久性领域中的事务,Transaction)中同样是极其强大的核心工程哲学。
- 隔离不再是天然的保护伞:在进程抽象中,不同的进程被地址空间隔离,一个进程死循环或崩溃不会影响另一个。但在多线程世界中,由于它们生活在同一个地址空间内,隔离被打破了。线程之间的互相伤害变得极其容易,这也极大地提高了并发编程的心智负担。
7. 多线程进程地址空间与竞态条件图解

导师的下一步建议: 我们已经清楚地看到了问题所在:不可控的调度将原本简单的代码切碎,引发了令人抓狂的竞态条件。 正如书中预告的,要解决这个问题,我们需要引入一种能把临界区“锁”起来的同步原语。
