发现无锁队列在某些机器上运行的时候并不能保证顺序,可能是什么问题?
这是一个非常典型的真实 Bug,根本原因几乎可以确定是 Memory Order 设置错误,配合不同 CPU 架构的内存模型差异才暴露出来。
先理解:为什么”某些机器”才出问题
不同 CPU 架构对内存操作的允许重排程度完全不同:
x86/x64 → TSO(Total Store Order) 允许的重排:只有 StoreLoad(写后读可以被重排) 不允许:StoreStore、LoadLoad、LoadStore ARM / ARM64 → 弱内存模型(Weakly Ordered) 几乎所有重排都允许: StoreStore(写-写可以被重排)← 这是锁-free queue 最常踩的坑 LoadLoad LoadStore StoreLoad PowerPC → 比 ARM 还弱
结论:用 relaxed 或者错误的 memory_order 写出来的无锁队列,在 x86 上可能恰好正确(x86 硬件帮你补了很多 barrier),放到 ARM 服务器或手机 SoC 上就开始出问题。这也是为什么很多 Bug 在开发机(x86)上测不出来,上线(ARM)才暴露。
最常见的具体 Bug:SPSC Queue 里的 StoreStore 重排
回到之前写的 SPSC Queue,看这个版本:
// 错误版本:tail 用了 relaxed
bool push(const T& item) {
size_t tail = tail_.load(std::memory_order_relaxed);
size_t next = (tail + 1) & MASK;
if (next == head_.load(std::memory_order_acquire))
return false;
buffer_[tail] = item; // ① 写数据
tail_.store(next, std::memory_order_relaxed); // ② 更新 tail(BUG:relaxed)
return true;
}
bool pop(T& item) {
size_t head = head_.load(std::memory_order_relaxed);
if (head == tail_.load(std::memory_order_relaxed)) // ③ 读 tail(BUG:relaxed)
return false;
item = buffer_[head]; // ④ 读数据
head_.store((head + 1) & MASK, std::memory_order_release);
return true;
}
在 ARM 上的问题时间线:
Producer(核心0) Consumer(核心1)
① buffer_[tail] = item
② tail_.store(next)
③ 看到 tail 更新了(tail != head)
④ 读 buffer_[head]
← 读到的是旧数据或垃圾值!
原因:ARM 允许 StoreStore 重排
CPU 实际执行顺序可能是:
② tail_.store(next) ← 先把 tail 写到其他核可见
① buffer_[tail] = item ← 数据写入还没传播过去
Consumer 看到 tail 已经更新,以为数据准备好了,实际上数据写入还没有传播到自己的 cache。
正确写法:
Producer(核心0) Consumer(核心1)
① buffer_[tail] = item
② tail_.store(next)
③ 看到 tail 更新了(tail != head)
④ 读 buffer_[head]
← 读到的是旧数据或垃圾值!
原因:ARM 允许 StoreStore 重排
CPU 实际执行顺序可能是:
② tail_.store(next) ← 先把 tail 写到其他核可见
① buffer_[tail] = item ← 数据写入还没传播过去
// acquire 语义:保证此行之后的所有读取(包括 buffer_ 的读)
// 能看到配对 release 之前的所有写入
if (head == tail_.load(std::memory_order_acquire)) // ✅
return false;
item = buffer_[head]; // 现在保证能读到正确数据
Acquire-Release 的本质:建立 happens-before 关系
Producer:
buffer_[tail] = item; ─┐
tail_.store(next, release); ┘ release:这两行有先后顺序,且对外可见
↕ synchronizes-with(跨线程的 happens-before)
Consumer:
tail_.load(acquire); ─┐
item = buffer_[head]; ─┘ acquire:这两行有先后顺序,且能看到上面的写
release + acquire 配对创建了一个跨线程的同步点:任何在 release store 之前的写入,对执行了对应 acquire load 的线程都是可见的。
MS Queue 里更隐蔽的 Memory Order 错误
多生产者多消费者的 Michael-Scott Queue 里,CAS 的 memory order 同样容易写错:
// 错误:CAS 成功时用 relaxed
if (tail->next.compare_exchange_weak(
next, node,
std::memory_order_relaxed, // ← 成功时应该是 release
std::memory_order_relaxed)) {
// ...
}
// 正确:
if (tail->next.compare_exchange_weak(
next, node,
std::memory_order_release, // 成功时:发布新节点,需要 release
std::memory_order_relaxed)) { // 失败时:只是重试,relaxed 够了
// ...
}
// 出队时读取节点数据:
Node* next = head->next.load(std::memory_order_acquire); // 需要 acquire
val = next->data; // 能看到入队时写入的 data
排查步骤
第一步:检查所有 atomic 操作的 memory_order
# 快速搜索代码里所有用了 relaxed 的地方 grep -n "memory_order_relaxed" your_queue.cpp
对每一处 relaxed 问自己:这个操作是否需要和其他线程的某个操作同步?如果是,就需要 acquire/release。
第二步:画出 happens-before 链
对每一对"写数据 → 通知 → 读数据"的模式,确认: 写数据 store(release) load(acquire) 读数据 ─────────── ≺ ──────────────── sync ──────────────── ≺ ────────── 必须在 release 之前 这里建立同步 必须在 acquire 之后
如果这条链断了(任何一环用了 relaxed),就是 Bug。
第三步:在 ARM 上加压测试
// 用 TSan(Thread Sanitizer)可以检测 data race
// 编译时加 -fsanitize=thread
// 或者在 ARM 机器上用 stress 工具加大并发压力:
for (int i = 0; i < 1000000; i++) {
producer_thread();
consumer_thread();
assert(consumed_value == produced_value); // 验证顺序
}
第四步:用 C++ 标准保证,不依赖平台行为
// 危险:依赖 x86 的强内存模型,换平台就出 Bug
std::atomic<int> flag{0};
flag.store(1, std::memory_order_relaxed); // 在 x86 上恰好不会被重排
// 安全:用标准规定的 memory order,所有平台行为一致
flag.store(1, std::memory_order_release);
一个检查 memory order 的思维框架
面对任意一个 atomic 操作,问三个问题:
1. 这个 store 之前,有没有普通内存写入需要对其他线程可见? → 如果有,用 release 2. 这个 load 之后,有没有普通内存读取依赖于"看到最新值"? → 如果有,用 acquire 3. 这个操作是否需要全局顺序一致(比如实现 mutex)? → 用 seq_cst(代价最高,非必要不用) 4. 以上都不是(只是单纯的计数器、统计)? → 才能用 relaxed
面试里如果被问到”你遇到过锁-free 的 Bug 吗”,这个场景是非常好的案例:在 x86 开发机上测试通过,部署到 ARM 的游戏服务器上出现数据错乱,最终定位到 SPSC Queue 的 tail_.store 用了 relaxed 而不是 release,在 ARM 的 StoreStore 重排下 Consumer 读到了写入前的数据。说清楚根因、排查过程、修复方案,是很有说服力的 Senior 级回答。
