原子加载前使用acquire fence是否合理?代码同步问题分析
问题背景
通常,acquire栅栏的标准用法是放在原子加载之后,确保后续读取非原子数据时能看到对应线程的写操作:
if (flag.load(std::memory_order_relaxed)) { std::atomic_thread_fence(std::memory_order_acquire); // 在此读取非原子数据 }
但如果把acquire栅栏放在原子加载之前,是否有实际意义?
以下是具体场景:共享缓冲区data和原子布尔变量flag在threadA与threadB间共享,data初始全为0。threadA先将flag设为true,再向data写入非零值;threadB先读取data,再检查flag:
#include <atomic> uint8_t data[4096] = {}; std::atomic<bool> flag = false; void threadA() { flag.store(true, std::memory_order_relaxed); std::atomic_thread_fence(std::memory_order_release); for (uint8_t &byte : data) { byte = 1; } } void threadB() { bool found_non_zero = false; for (uint8_t byte : data) { if (byte) { found_non_zero = true; } } std::atomic_thread_fence(std::memory_order_acquire); bool flag_observed = flag.load(std::memory_order_relaxed); if (found_non_zero) { ASSERT_TRUE(flag_observed); } }
能否保证:若threadB读取到data中的非零值,则必然观察到flag已设为true(即断言始终成功)?
你的预期是:threadA中的release栅栏阻止flag.store被重排到data写入之后,threadB中的acquire栅栏阻止flag.load被重排到data读取之前。但根据C++标准,acquire栅栏仅在原子加载之后才有效,release栅栏仅在原子存储之前才有效,这似乎和预期矛盾。
当前代码的问题
当前代码无法保证断言始终成功,核心原因有两点:
- 仅保证线程内顺序,未解决跨线程可见性:
release栅栏确实能阻止flag.store被重排到data写入之后,acquire栅栏也能阻止flag.load被重排到data读取之前,但这只是单线程内的执行顺序约束,无法保证threadA的操作对threadB的可见性顺序——threadB可能先看到data的非零值(缓存同步),但未看到flag的更新。 - 未建立合法的release-acquire同步:根据C++标准,
release栅栏需要与后续的原子存储操作配对,acquire栅栏需要与前置的原子加载操作配对,才能建立跨线程的同步关系。当前代码中,release栅栏后只有非原子的data写入,acquire栅栏前只有非原子的data读取,没有对应的原子操作作为同步点,因此无法保证操作的跨线程可见性。
修复方案
方案一:使用原子操作的release/acquire语义(推荐)
直接调整flag的存储和加载内存顺序,通过原子操作本身建立同步关系,无需额外栅栏:
#include <atomic> uint8_t data[4096] = {}; std::atomic<bool> flag = false; void threadA() { // release语义:阻止后续的data写入被重排到该存储之前,确保flag.store先执行 flag.store(true, std::memory_order_release); for (uint8_t &byte : data) { byte = 1; } } void threadB() { bool found_non_zero = false; for (uint8_t byte : data) { if (byte) { found_non_zero = true; } } // acquire语义:阻止前置的data读取被重排到该加载之后,同时建立可见性约束 // 若threadB读到data非零,说明threadA的data写入已完成,flag.store必然已执行且对threadB可见 bool flag_observed = flag.load(std::memory_order_acquire); if (found_non_zero) { ASSERT_TRUE(flag_observed); } }
方案二:使用seq_cst栅栏保证全局顺序
如果必须依赖栅栏实现,可以用memory_order_seq_cst栅栏强制全局操作顺序,确保所有线程看到的操作顺序一致:
#include <atomic> uint8_t data[4096] = {}; std::atomic<bool> flag = false; void threadA() { flag.store(true, std::memory_order_relaxed); // seq_cst栅栏:强制前置的flag.store不会被重排到后续的data写入之后 std::atomic_thread_fence(std::memory_order_seq_cst); for (uint8_t &byte : data) { byte = 1; } } void threadB() { bool found_non_zero = false; for (uint8_t byte : data) { if (byte) { found_non_zero = true; } } // seq_cst栅栏:强制后续的flag.load不会被重排到前置的data读取之前 std::atomic_thread_fence(std::memory_order_seq_cst); bool flag_observed = flag.load(std::memory_order_relaxed); if (found_non_zero) { ASSERT_TRUE(flag_observed); } }
memory_order_seq_cst会为所有线程建立全局操作顺序,若threadB读到data的非零值,说明threadA的data写入已在全局顺序中完成,那么threadA的flag.store必然早于该写入,threadB读取flag时必然能看到true。不过这种方式开销比release/acquire更大,仅在特殊场景下使用。
内容的提问来源于stack exchange,提问作者cyb0124

