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),导致逻辑错误。
1.5 原子操作与内存屏障的协同关系
原子操作和内存屏障是并发控制的两大基石,它们的关系如下:
- 原子操作保证“读-改-写”的不可分割性,但不保证内存顺序。
- 内存屏障保证“顺序”与“可见性”,但不保证操作的原子性。
- 带屏障的原子操作(如
atomic_add_return()在大多数架构上隐含内存屏障)同时提供两种保证。
在 Linux 内核中,RCU(Read-Copy-Update)机制正是利用内存屏障和原子操作的组合,实现了读端几乎零开销的并发控制——这正是本课程后续章节的核心主题。
工程实践要点:
- 优先使用内核提供的原子操作 API,避免手写内联汇编。
- 在需要顺序保证时,使用
smp_load_acquire()/smp_store_release()替代显式屏障,语义更清晰。 - 使用
lockdep工具检测潜在的加锁顺序问题,避免死锁。 - 在性能关键路径上,考虑使用 RCU 或无锁数据结构替代读写锁。