4-3.【OC】【锁】什么是 False Sharing?在锁设计中如何避免?

3 阅读3分钟

False Sharing(伪共享) 是多核编程中一个极其隐蔽的性能杀手。它发生在这种场景:两个完全无关的变量,因为恰好被编译器或分配器放到了同一个 Cache Line(缓存行) 中,导致本应并行的多线程操作变成了事实上串行的“总线拉锯战”。


1. 为什么会发生 False Sharing?

理解这个问题的关键在于:CPU 缓存同步的最小单位不是字节,而是缓存行(通常为 64 字节)。

  1. 同住一屋: 假设线程 A 只需要操作变量 X,线程 B 只需要操作变量 Y。但由于 XY 在内存中紧挨着,它们被同时载入了同一个缓存行。

  2. MESI 惩罚: * 核心 1 修改了 X,根据 MESI 协议,它必须将核心 2 对应的整个缓存行设为 Invalid (无效)

    • 核心 2 想要修改 Y 时,发现缓存行失效,必须重新从主内存(或核心 1 的缓存)拉取数据。
  3. 结果: 尽管 XY 逻辑上毫无关系,但两个核心会为了争夺这块缓存行的“修改权”而频繁打架,产生巨大的总线开销。


2. 在锁设计中如何避免?

在设计高性能锁(如自旋锁或无锁队列)时,避免伪共享是进入“工业级”门槛的标志。常见策略如下:

A. 缓存行对齐(Padding / Alignment)

这是最直接的方法:在两个频繁修改的变量之间插入“无用”的填充数据,强行将它们推到不同的缓存行。

  • 手动填充: 在 C/C++ 中增加无用的数组。

    C++

    struct MyLock {
        volatile int lockA;
        long long padding[8]; // 填充 64 字节,确保 lockB 另起一行
        volatile int lockB;
    };
    
  • 语言特性支持: * C++11: 使用 alignas(64)

    • Java 8+: 使用 @Contended 注解(需要开启 JVM 参数)。
    • Swift: 使用 managedBuffer 或手动对齐分配。

B. 空间换时间(Array Padding)

在处理锁数组(如 Striped Lock)时,如果数组元素是连续的 int,竞争会非常严重。

  • 方案: 不要直接定义 int locks[16],而是定义一个结构体,让每个结构体的大小刚好等于一个缓存行(64 Bytes)。

C. 局部化数据(Local Copy)

尽可能让每个线程操作自己私有的副本,只在最后阶段进行合并(Reduce)。

  • Per-CPU 变量: Linux 内核中广泛使用,每个核心拥有独立的计数器,彻底消除 MESI 协议在核心间的通信损耗。

3. 实战案例:Disruptor 框架

高性能并发框架 LMAX Disruptor 之所以快,很大程度上归功于它对缓存行的极致利用。它的 Sequence 类通过在左右两边各填充 7 个 long 变量(共 56 字节),配合自带的一个 long 变量(8 字节),完美占据了一个 64 字节的缓存行,确保无论这个序列号如何高频更新,都不会干扰到旁边的变量。


总结

  • 现象: 无关变量“躺枪”,导致缓存频繁失效。
  • 本质: MESI 协议以缓存行为单位进行颗粒度控制的副作用。
  • 对策: 对齐(Alignment)与填充(Padding),让核心变量“独占一屋”。