1. 同步互斥概述:并发编程中的竞态条件、临界区概念、同步与互斥的基本原理
并发编程是操作系统与高性能软件的核心技术,但并发带来的不确定性也是绝大多数 Bug 的根源。本章从最基础的竞态条件与临界区出发,建立同步互斥的理论框架,为后续深入分析 Linux 内核的 futex 与读写锁实现打下坚实基础。
1.1 并发与竞态条件
当多个执行流(线程或进程)同时访问共享数据,且至少有一个执行流执行写操作时,最终结果依赖于这些执行流的精确时序,这种现象称为 竞态条件(Race Condition)。
竞态条件的本质: 程序的正确性依赖于“运气”,而非逻辑保证。同一份代码,在不同调度时机下可能产生完全不同的结果。
考虑一个经典例子:两个线程同时对一个全局计数器执行 counter++ 操作。该操作在机器指令层面通常分解为三步:
- LOAD:从内存读取 counter 到寄存器
- ADD:寄存器值加 1
- STORE:将寄存器值写回内存
如果两个线程的 LOAD、ADD、STORE 指令交错执行,就会发生“丢失更新”:
典型丢失更新场景: 线程 A 执行 LOAD 得到 5,线程 B 执行 LOAD 也得到 5;A 执行 ADD 得到 6 并 STORE 回内存(counter=6);B 执行 ADD 得到 6 并 STORE 回内存(counter=6)。两次递增后,counter 本应为 7,实际却为 6。
下面的 SVG 图展示了竞态条件下两条指令流的交错执行过程:
1.2 临界区与互斥
为了消除竞态条件,必须将访问共享数据的代码段保护起来,使得同一时刻最多只有一个执行流可以进入。这段需要被保护的代码区域称为 临界区(Critical Section)。
临界区的访问需要满足以下四个条件:
| 条件 | 说明 | 违反后果 |
|---|---|---|
| 互斥(Mutual Exclusion) | 同一时刻最多只有一个线程在临界区内 | 竞态条件、数据损坏 |
| 前进(Progress) | 没有线程在临界区内时,必须允许一个等待线程进入 | 死锁、活锁 |
| 有限等待(Bounded Waiting) | 线程不能无限等待进入临界区 | 饥饿(Starvation) |
| 无忙等(Optional) | 等待时不应消耗 CPU 循环(现代实现通常避免忙等) | CPU 资源浪费 |
常见误区: 临界区越小越好,但过小的临界区可能导致频繁的加锁/解锁开销;临界区过大则会降低并发度。实际工程中需要在安全性与性能之间权衡。
下面的 SVG 图展示了互斥锁保护临界区的基本模型:
1.3 同步与互斥的关系
互斥(Mutual Exclusion) 和 同步(Synchronization) 是并发控制的两个核心维度,既有区别又紧密关联:
| 概念 | 核心目标 | 典型问题 | 实现机制 |
|---|---|---|---|
| 互斥 | 保护共享资源不被同时访问 | 竞态条件、数据不一致 | 互斥锁(mutex)、自旋锁(spinlock) |
| 同步 | 协调多个执行流的执行顺序 | 生产者-消费者、屏障等待 | 信号量(semaphore)、条件变量(condition variable) |
关键理解: 互斥是同步的一种特殊形式——互斥保证“不能同时”,而同步保证“在正确的时刻”。在 Linux 内核中,futex 机制同时支持了互斥锁和条件变量的高效实现。
下面的 SVG 图对比了互斥与同步的典型场景:
1.4 同步互斥的实现层次
在现代操作系统中,同步互斥机制从硬件到软件分为多个层次,每一层为上一层提供更高级的抽象:
- 硬件层:提供原子指令(如 x86 的
LOCK前缀、CMPXCHG、ARM 的LDREX/STREX) - 内核层:基于原子指令实现自旋锁、互斥锁、信号量、futex 等
- 系统调用层:futex 系统调用将用户态快速路径与内核态阻塞/唤醒结合
- 用户态库层:pthread 库、glibc 等封装 futex 提供 POSIX 线程同步 API
课程聚焦: 本课程的核心是 内核层 的 futex 与读写锁实现。futex(Fast Userspace Mutex)是 Linux 内核提供的高效同步原语,它将“无竞争时的快速路径”放在用户态完成,仅在需要阻塞或唤醒时才陷入内核,从而实现了极致的性能。
下面的 SVG 图展示了同步互斥的层次化架构:
1.5 本章小结
本章建立了同步互斥的核心概念框架:
- 竞态条件 是并发编程中数据不一致的根本原因,源于指令交错执行
- 临界区 是需要互斥保护的代码段,必须满足互斥、前进、有限等待三个条件
- 互斥 防止同时访问,同步 协调执行顺序,两者共同构成并发控制的基础
- 同步互斥的实现从硬件原子指令到内核原语再到用户态库,形成清晰的层次化架构
- Linux 的 futex 机制位于系统调用层,是连接用户态快速路径与内核态阻塞唤醒的关键桥梁