1. 并发编程基础:并发与并行的区别、临界区与竞态条件、原子操作的概念
并发编程是锁竞争分析与调优的基石。在深入 spinlock、RCU 等同步机制之前,必须准确理解并发与并行的本质区别、临界区与竞态条件的触发场景,以及原子操作这一底层构建块。本章从这三个核心概念出发,为后续的锁分析建立坚实的理论基础。
1.1 并发与并行的区别
并发(Concurrency)与并行(Parallelism)常被混用,但在操作系统和硬件层面有严格区分:
| 维度 | 并发(Concurrency) | 并行(Parallelism) |
|---|---|---|
| 定义 | 多个任务在同一时间段内交替执行(逻辑上同时) | 多个任务在同一时刻真正同时执行(物理上同时) |
| 硬件基础 | 单核 CPU 通过时间片轮转即可实现 | 需要多核 CPU 或多处理器系统 |
| 执行方式 | 任务切换(上下文切换) | 任务同时运行在不同核心上 |
| 关注点 | 任务间的协调与资源竞争 | 任务间的数据拆分与负载均衡 |
| 典型场景 | 单核服务器处理大量网络连接 | 多核 CPU 并行计算矩阵乘法 |
关键理解:并发是程序设计的性质(如何组织任务),并行是执行时的性质(任务是否同时运行)。一个并发程序可以在单核上运行,也可以在多核上并行运行。
下面这张 SVG 图直观展示了并发与并行的执行差异:
1.2 临界区与竞态条件
当多个执行流(线程/进程/中断处理程序)访问共享资源时,就会引入两个核心问题:临界区和竞态条件。
1.2.1 临界区(Critical Section)
临界区是指访问共享资源的代码片段,这段代码在同一时刻只能被一个执行流执行。如果多个执行流同时进入临界区,就会导致数据不一致。
临界区识别原则:任何对全局变量、静态变量、硬件寄存器、文件偏移等共享资源的读-修改-写操作序列,都需要作为临界区保护。
典型的临界区示例:
/* 共享变量 */
static int shared_counter = 0;
/* 临界区:对 shared_counter 的读-修改-写操作 */
void increment_counter(void) {
/* 进入临界区前需要加锁 */
int tmp = shared_counter; /* 读 */
tmp = tmp + 1; /* 修改 */
shared_counter = tmp; /* 写 */
/* 离开临界区后需要解锁 */
}
1.2.2 竞态条件(Race Condition)
竞态条件是指程序的执行结果依赖于多个执行流的相对执行顺序,当这个顺序不可控时,就会产生非预期的结果。竞态条件是临界区未正确保护时的直接后果。
下面通过一个经典例子说明竞态条件:
1.3 原子操作的概念
原子操作(Atomic Operation)是指不可被中断的操作,要么全部执行完成,要么完全不执行,不会出现中间状态。原子操作是构建锁和同步机制的最底层基础。
1.3.1 原子操作的本质
在硬件层面,原子操作通常由 CPU 指令直接支持,例如 x86 架构下的 LOCK 前缀指令、ARM 架构下的 LDREX/STREX 指令对。这些指令保证在操作过程中,其他 CPU 核心或线程无法干扰。
| 架构 | 原子指令示例 | 说明 |
|---|---|---|
| x86/x86_64 | LOCK CMPXCHG |
原子比较并交换,带 LOCK 前缀保证总线锁定 |
| x86/x86_64 | LOCK XADD |
原子加并返回原值 |
| ARMv7/v8 | LDREX/STREX |
加载-独占/存储-独占,通过独占监视器实现原子性 |
| RISC-V | LR/SC |
加载保留/条件存储,类似 ARM 的 LDREX/STREX |
1.3.2 Linux 内核中的原子操作 API
Linux 内核封装了平台无关的原子操作接口,位于 <asm/atomic.h> 和 <linux/atomic.h>:
/* 原子变量类型 */
atomic_t v;
/* 初始化 */
atomic_set(&v, 0);
/* 原子操作 */
atomic_inc(&v); /* v++ */
atomic_dec(&v); /* v-- */
atomic_add(5, &v); /* v += 5 */
atomic_sub(3, &v); /* v -= 3 */
/* 原子读与写 */
int val = atomic_read(&v); /* 原子读取 */
atomic_set(&v, 10); /* 原子写入 */
/* 原子比较并交换(CAS) */
int old = atomic_cmpxchg(&v, expected, new);
重要提示:普通 C 语言的
counter++ 不是原子操作,它在底层对应三条指令:读(load)、加(add)、写(store)。在多核环境下,这三条指令之间可能被其他核心打断,导致竞态条件。必须使用 atomic_inc() 等内核 API 保证原子性。
1.3.3 原子操作与锁的关系
原子操作是锁机制的底层构建块。下图展示了从硬件原子指令到高层锁机制的层次关系:
1.4 本章小结
- 并发 vs 并行:并发是逻辑上的同时执行(单核时间片),并行是物理上的同时执行(多核)。锁竞争分析主要关注并发场景下的资源争抢。
- 临界区:访问共享资源的代码段,必须通过同步机制保证互斥访问。
- 竞态条件:未保护临界区导致的执行结果不确定性,是锁竞争问题的根源。
- 原子操作:不可中断的操作,由硬件指令直接支持,是 spinlock、mutex、RCU 等所有同步机制的底层基石。