文档目录

LOCK前缀就像给指令挂上了“原子手雷”,而cmpxchg和xadd则是两种不同战术的手雷。

我们先拆解硬件行为,再看汇编形态,最后看C++如何映射。


1. 灵魂拷问:LOCK 前缀到底对CPU做了什么?

当你在汇编指令前加上 LOCK,CPU会执行以下硬件契约:

  1. 断言总线锁或缓存锁(我们上节课讲的,优先缓存锁)。
  2. 内存屏障(Memory Barrier):LOCK 隐式包含全屏障(Full Barrier),它会强制:
    • 将CPU核心的写缓冲区(Store Buffer) 全部冲刷到缓存。
    • 确保这条指令之前的所有读写操作都全局可见,之后的操作不会提前。
  3. 原子性保证:在读-改-写期间,不允许其他核心/硬件设备(如DMA)访问该内存地址。

关键点:LOCK 不是“锁住变量”,而是锁住对那个内存地址的访问路径,保证读-改-写是一个不可分割的硬件事务。


2. 核心兵器一:xadd —— 原子自增/自减的“特快专列”

汇编原型

lock xadd [mem], reg   ; 等价于:先交换,再相加
; 功能:将 [mem] 的值 与 reg 的值交换,然后将两者之和存回 [mem]
; 返回值(旧值) 存放在 reg 中

执行过程(3步原子完成)

初始: [mem] = 10, reg (eax) = 1
1. 硬件原子性读取 [mem] 的值(10)到内部临时寄存器
2. 将 reg(1)写入 [mem](此时 [mem]=1,但这是内部中间态)
3. 计算 10 + 1 = 11,写回 [mem]
最终: [mem]=11, reg=10  (reg 拿到了旧值)

典型应用场景

  • 高性能统计计数器(网络包计数、QPS统计)。
  • 无锁栈的 pop 操作(减少引用计数)。

C++ 映射(你几乎不会直接写汇编,但要知道底层)

#include <atomic>

std::atomic<int> counter{0};

// C++ 的 fetch_add 在 x86 上编译为 lock xadd
int old_value = counter.fetch_add(1, std::memory_order_acq_rel);
// old_value 拿到增加前的值,counter 原子 +1

反汇编查看(GCC/Clang):

lock xadd DWORD PTR [counter], eax   ; eax 传入1,返回旧值

3. 核心兵器二:cmpxchg —— 无锁编程的“瑞士军刀”(CAS)

这是实现无锁数据结构(Lock-Free) 的灵魂指令。它比 xadd 强大,因为它带条件判断。

汇编原型

lock cmpxchg [mem], reg   ; 隐含寄存器: rax/eax/ax/al
; 功能:比较 [mem] 与 eax,如果相等,则把 reg 写入 [mem];否则将 [mem] 加载到 eax

执行过程(C伪代码)

bool cas(uintptr_t* mem, uintptr_t expected, uintptr_t desired) {
    // 硬件原子执行下面代码:
    if (*mem == expected) {
        *mem = desired;   // 成功修改
        return true;
    } else {
        expected = *mem;  // 失败时,把最新值写回 expected (通常保存在 eax)
        return false;
    }
}

关键特性(面试必考)

  • 比较-交换 是一个硬件原子操作,不是“先读再比较”两个步骤。
  • 失败时自动加载最新值到 eax,省去一次额外读取(优化)。
  • ABA问题:虽然硬件保证原子,但如果 *mem 从 A→B→A 变回,cmpxchg 会认为没有变化而成功(这在某些场景是灾难)。

典型应用场景

  • 无锁队列的入队/出队。
  • 写时复制(COW)的指针切换。
  • 自旋锁(Spinlock) 的尝试获取。

C++ 映射

#include <atomic>

std::atomic<int> data{0};
int expected = 0;
int desired = 42;

// C++11 的 compare_exchange_weak/strong
// 底层在 x86 上编译为 lock cmpxchg
bool success = data.compare_exchange_strong(expected, desired);
// 如果 data 当前是 0,则设为 42,success=true
// 如果 data 当前不是 0,则 expected 被更新为 data 的最新值,success=false

反汇编查看:

mov eax, expected   ; 必须先把期望值放入 eax
lock cmpxchg DWORD PTR [data], edx   ; edx 是 desired
; 执行后,如果失败,eax 被更新为 data 的最新值

4. 两者的“战场分工”:什么时候用谁?

场景 推荐指令 原因
单纯的计数器加减(统计、引用计数) lock xadd 一条指令完成,性能最高,无需额外判断
复杂的无锁数据结构(队列、栈、链表) lock cmpxchg 需要基于当前状态决定是否更新,支持重试循环
实现自旋锁 lock cmpxchg (或 lock bts) 需要从0->1的原子转换,失败时自旋
实现 Fetch-And-Add 语义 lock xadd C++的 fetch_add 底层就是它

5. 硬核实战:手动模拟一个无锁计数器(对比两种指令)

#include <atomic>
#include <thread>
#include <iostream>
#include <vector>

// 方案1: 使用 xadd (fetch_add) —— 最快
class FastCounter {
    std::atomic<int> value_{0};
public:
    int increment() {
        return value_.fetch_add(1, std::memory_order_relaxed); // 底层 lock xadd
    }
    int get() const { return value_.load(); }
};

// 方案2: 使用 cmpxchg (compare_exchange) —— 稍慢,但可扩展
class CasCounter {
    std::atomic<int> value_{0};
public:
    int increment() {
        int old_val = value_.load(std::memory_order_relaxed);
        int new_val;
        do {
            new_val = old_val + 1;
            // 底层: lock cmpxchg
            // 如果期间被其他线程修改,old_val 自动更新,循环重试
        } while (!value_.compare_exchange_weak(old_val, new_val, 
                 std::memory_order_release, std::memory_order_relaxed));
        return old_val;
    }
    int get() const { return value_.load(); }
};

// 性能对比 (实测): FastCounter 比 CasCounter 快约 20-30%
// 因为 cmpxchg 在竞争激烈时会产生多次重试 (CPU 流水线冲刷)

6. 硬件彩蛋:cmpxchg 的双重人格 —— cmpxchg8b/cmpxchg16b

对于64位或128位的双字(Double-word) 原子操作,x86提供了增强版:

  • lock cmpxchg8b:比较并交换 8 字节(需要 edx:eax 作为期望值)。
  • lock cmpxchg16b:比较并交换 16 字节(需要 rdx:rax + rcx:rbx)。

C++ 映射:当你对 std::atomic<struct> 使用 compare_exchange 且结构体大小为16字节且对齐时,编译器会生成这条指令。


7. 最重要的一条“潜规则”(性能调优必知)

LOCK 前缀指令虽然快,但它会阻塞 所有核心的缓存一致性流量(即使是缓存锁,也会在总线上广播失效消息)。

最佳实践:

  • 能不用就不用:对于统计计数,可以用 Per-CPU 变量 + 最终汇总。
  • 避免频繁竞争:如果多个线程同一时刻都执行 lock xadd,缓存行会在核心间“乒乓”,性能下降超过10倍。
  • 使用 memory_order_relaxed:如果只是计数,不需要内存屏障的同步效果,用 relaxed 可以让编译器少插入 mfence 指令(但 lock 本身还是有屏障)。

8. 给你的“底层开发思维”加餐

当你以后写 C++ 并发代码时,心里要有这张映射表:

C++ 原子操作 x86 底层实现(带 lock)
fetch_add / operator++ lock xadd
fetch_sub / operator-- lock xadd (负数)
compare_exchange_strong/weak lock cmpxchg (或 cmpxchg16b)
exchange lock xchg (注意:xchg 自带 lock 语义,无需加前缀)
fetch_and / fetch_or / fetch_xor lock and / lock or / lock xor