1. 并发编程基础:并发与并行的区别、临界区与竞态条件、原子操作的概念

并发编程是锁竞争分析与调优的基石。在深入 spinlock、RCU 等同步机制之前,必须准确理解并发与并行的本质区别、临界区与竞态条件的触发场景,以及原子操作这一底层构建块。本章从这三个核心概念出发,为后续的锁分析建立坚实的理论基础。

1.1 并发与并行的区别

并发(Concurrency)与并行(Parallelism)常被混用,但在操作系统和硬件层面有严格区分:

维度 并发(Concurrency) 并行(Parallelism)
定义 多个任务在同一时间段内交替执行(逻辑上同时) 多个任务在同一时刻真正同时执行(物理上同时)
硬件基础 单核 CPU 通过时间片轮转即可实现 需要多核 CPU 或多处理器系统
执行方式 任务切换(上下文切换) 任务同时运行在不同核心上
关注点 任务间的协调与资源竞争 任务间的数据拆分与负载均衡
典型场景 单核服务器处理大量网络连接 多核 CPU 并行计算矩阵乘法
关键理解:并发是程序设计的性质(如何组织任务),并行是执行时的性质(任务是否同时运行)。一个并发程序可以在单核上运行,也可以在多核上并行运行。

下面这张 SVG 图直观展示了并发与并行的执行差异:

并发 vs 并行 执行对比 并发(单核时间片轮转) 任务A 任务B 任务A 任务B 时间片 → 交替执行,逻辑上“同时”进行 并行(多核同时执行) 核心1:任务A 核心2:任务B 同一时刻,两个任务在不同核心上真正同时运行 时间维度 并发编程中的常见误区 • 误区1:并发 = 并行。实际上,并发是任务组织方式,并行是执行方式。 • 误区2:多线程一定比单线程快。并发引入上下文切换开销,需权衡。 • 误区3:锁只解决并行问题。并发场景下同样存在竞态条件,需要锁保护。

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)

竞态条件是指程序的执行结果依赖于多个执行流的相对执行顺序,当这个顺序不可控时,就会产生非预期的结果。竞态条件是临界区未正确保护时的直接后果。

下面通过一个经典例子说明竞态条件:

竞态条件示例:两个线程同时执行 counter++ 初始:counter = 0 线程A:读 counter = 0 线程B:读 counter = 0 线程A:写 counter = 1 线程B:写 counter = 1 错误结果 counter = 1 (期望值 = 2) 竞态条件分析 两个线程同时读取 counter=0,各自加 1 后写回,导致两次递增只产生一次效果(丢失更新)。

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 原子操作与锁的关系

原子操作是锁机制的底层构建块。下图展示了从硬件原子指令到高层锁机制的层次关系:

原子操作在锁机制中的层次结构 应用层:spinlock / mutex / RCU / rwlock 内核抽象层:atomic_t / atomic_long_t / atomic64_t 硬件指令层:LOCK CMPXCHG / LDREX-STREX / LR-SC 层次说明 • 上层锁依赖 原子操作实现 • 原子操作封装 硬件指令差异 • 硬件指令保证 缓存一致性

1.4 本章小结

  • 并发 vs 并行:并发是逻辑上的同时执行(单核时间片),并行是物理上的同时执行(多核)。锁竞争分析主要关注并发场景下的资源争抢。
  • 临界区:访问共享资源的代码段,必须通过同步机制保证互斥访问。
  • 竞态条件:未保护临界区导致的执行结果不确定性,是锁竞争问题的根源。
  • 原子操作:不可中断的操作,由硬件指令直接支持,是 spinlock、mutex、RCU 等所有同步机制的底层基石。