LOCK前缀就像给指令挂上了“原子手雷”,而cmpxchg和xadd则是两种不同战术的手雷。
我们先拆解硬件行为,再看汇编形态,最后看C++如何映射。
1. 灵魂拷问:LOCK 前缀到底对CPU做了什么?
当你在汇编指令前加上 LOCK,CPU会执行以下硬件契约:
- 断言总线锁或缓存锁(我们上节课讲的,优先缓存锁)。
- 内存屏障(Memory Barrier):
LOCK隐式包含全屏障(Full Barrier),它会强制:- 将CPU核心的写缓冲区(Store Buffer) 全部冲刷到缓存。
- 确保这条指令之前的所有读写操作都全局可见,之后的操作不会提前。
- 原子性保证:在读-改-写期间,不允许其他核心/硬件设备(如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 |