1. 并发控制基础

1.1 并发编程的核心挑战

在现代多核处理器系统中,并发编程是提升系统吞吐量的关键手段,但同时也引入了三大核心挑战:

挑战维度 描述 典型后果
竞态条件 多个执行流对共享资源的访问顺序不可预测,导致结果依赖调度时序 数据损坏、程序崩溃、安全漏洞
原子性缺失 看似单条指令的操作在底层被拆分为多条微操作,中途可被中断 变量更新丢失、状态不一致
可见性与有序性 CPU 缓存与编译器重排序导致一个线程的写入对另一个线程不可见 死循环、逻辑错误、难以复现的 Bug
关键认知: 并发 Bug 的复现难度与系统负载、CPU 核心数、编译器优化等级呈指数级增长。依赖“碰巧正确”的代码在生产环境中必然失败。

1.2 临界区与竞态条件

临界区(Critical Section)是指访问共享资源的代码片段,同一时间只允许一个执行流进入。竞态条件(Race Condition)则发生在多个执行流同时进入临界区,且最终结果取决于执行时序的场景。

以下是一个典型的竞态条件示例:

/* 全局共享计数器 */
static int shared_counter = 0;

/* 两个线程同时执行此函数 */
void increment_counter(void)
{
    /* 读取-修改-写入:非原子操作 */
    int tmp = shared_counter;   // 步骤1:读取
    tmp = tmp + 1;              // 步骤2:修改
    shared_counter = tmp;       // 步骤3:写入
}

当线程 A 执行完步骤1(读取到 0)后被调度出去,线程 B 完整执行三次步骤后计数器变为 1,线程 A 恢复执行步骤2、3,将计数器写回 1——两次递增操作最终只增加了 1,这就是典型的“丢失更新”竞态。

工程陷阱: 即使使用 volatile 关键字,也无法解决多核环境下的竞态条件。volatile 仅阻止编译器优化,不提供原子性保证,也不插入内存屏障。

1.3 原子操作

原子操作(Atomic Operation)是不可中断的操作单元,要么全部执行成功,要么完全不执行。在 Linux 内核中,原子操作分为两类:

类型 API 示例 适用场景
原子整数操作 atomic_inc()、atomic_add_return() 计数器、引用计数、标志位
原子位操作 set_bit()、test_and_set_bit() 位图管理、自旋锁实现
原子比较交换 cmpxchg()、atomic_cmpxchg() 无锁数据结构、乐观锁

使用原子操作改写上述计数器:

#include <linux/atomic.h>

static atomic_t shared_counter = ATOMIC_INIT(0);

void increment_counter_safe(void)
{
    atomic_inc(&shared_counter);  // 硬件保证的原子递增
}
性能提示: 原子操作通常比普通内存操作慢 2-5 倍(取决于 CPU 架构),因为它们需要锁定缓存行或总线。在性能敏感路径中,应尽量减少原子操作的频率。

1.4 内存屏障

内存屏障(Memory Barrier)是 CPU 或编译器指令,用于强制内存访问的顺序约束。现代处理器为了提升性能,会进行指令重排序(Out-of-Order Execution)和缓存一致性延迟,导致内存操作的实际执行顺序与程序顺序不一致。

内存屏障的四种基本类型:

屏障类型 Linux API 语义说明
读屏障 smp_rmb() 屏障前的所有读操作必须在屏障后的读操作之前完成
写屏障 smp_wmb() 屏障前的所有写操作必须在屏障后的写操作之前完成
通用屏障 smp_mb() 屏障前的所有读写操作必须在屏障后的读写操作之前完成
获取-释放语义 smp_load_acquire() / smp_store_release() 获取语义防止后续读写越过屏障;释放语义防止前序读写越过屏障

以下是一个典型的生产者-消费者场景,展示内存屏障的必要性:

/* 共享数据 */
int data;
int flag = 0;

/* 生产者线程 */
void producer(void)
{
    data = 42;                  // 步骤1:写入数据
    smp_wmb();                  // 写屏障:确保 data 写入在 flag 写入之前可见
    flag = 1;                   // 步骤2:设置标志
}

/* 消费者线程 */
void consumer(void)
{
    while (flag == 0)           // 步骤3:轮询标志
        cpu_relax();
    smp_rmb();                  // 读屏障:确保 flag 读取后,data 读取可见
    printk("data = %d\n", data); // 步骤4:读取数据
}

如果没有 smp_wmb() 和 smp_rmb(),消费者可能看到 flag == 1 但 data 仍然是旧值(0),导致逻辑错误。

内存屏障在生产者-消费者模型中的作用 生产者线程 ① data = 42 ② smp_wmb() 写屏障 ③ flag = 1 消费者线程 ④ while(flag == 0) 轮询 ⑤ smp_rmb() 读屏障 ⑥ printk(data) → 42 共享内存(可能被 CPU 缓存重排序) data = 42 | flag = 1 写屏障保证 data 写入在 flag 写入之前全局可见 没有屏障时:消费者可能看到 flag=1 但 data=0(重排序导致)

1.5 原子操作与内存屏障的协同关系

原子操作和内存屏障是并发控制的两大基石,它们的关系如下:

  • 原子操作保证“读-改-写”的不可分割性,但不保证内存顺序。
  • 内存屏障保证“顺序”与“可见性”,但不保证操作的原子性。
  • 带屏障的原子操作(如 atomic_add_return() 在大多数架构上隐含内存屏障)同时提供两种保证。

在 Linux 内核中,RCU(Read-Copy-Update)机制正是利用内存屏障和原子操作的组合,实现了读端几乎零开销的并发控制——这正是本课程后续章节的核心主题。

工程实践要点:
  1. 优先使用内核提供的原子操作 API,避免手写内联汇编。
  2. 在需要顺序保证时,使用 smp_load_acquire() / smp_store_release() 替代显式屏障,语义更清晰。
  3. 使用 lockdep 工具检测潜在的加锁顺序问题,避免死锁。
  4. 在性能关键路径上,考虑使用 RCU 或无锁数据结构替代读写锁。