对齐至原生字长的int与std::atomic<int>:用前者替代后者始终安全吗?
#include <atomic> #include <thread> alignas(sizeof(void*)) int n1; std::atomic<int> n2; int main() { std::thread threads[32]; for (auto& thread : threads) { thread = std::thread([] { while (true) { // randomly load and/or store n1, n2 } }); } for (auto& thread : threads) { thread.join(); } }
针对上述代码的分析:
n1对齐至原生字边界,因此在汇编指令层面无需LOCK前缀即可实现原子加载与存储。n2是std::atomic<int>类型,不确定其在汇编指令层面是否会使用LOCK前缀。
问题
为获取最佳性能收益,使用对齐至原生字长的 int 变量替代 std::atomic<int> 变量是否始终安全?
回答
绝对不能这样做,这种替代方式并不安全,核心理由如下:
语言标准层面的原子性保障缺失
C++标准仅保证std::atomic系列类型的操作是原子的。普通int即使在硬件层面能实现原子读写,编译器仍可能对其进行优化(比如将变量缓存到寄存器,多次读写不同步回内存),导致线程间的操作出现非原子行为——比如看似原子的读写被拆分为多个指令,或被重排序。内存可见性与指令重排序问题
普通int的读写没有内存顺序语义,编译器和CPU可自由对这些操作重排序,也不会生成内存屏障同步缓存。这意味着一个线程对n1的写入,其他线程可能永远看不到最新值,或看到乱序的操作结果,彻底破坏线程间的数据一致性。
而std::atomic<int>的操作会根据指定内存顺序(默认是memory_order_seq_cst)生成必要的内存屏障,保证操作的可见性和有序性。即便x86平台上对齐的std::atomic<int>读写不会用到LOCK前缀,它也能提供语言层面的内存模型保障。平台依赖性与可移植性
“对齐至原生字边界的int无需LOCK前缀即可原子操作”是x86等特定平台的硬件特性,并非所有CPU架构都支持。比如ARM、PowerPC等弱内存模型架构上,即使对齐的字操作也需要显式内存屏障才能保证原子性和可见性,依赖硬件特性的代码完全不具备可移植性。
补充:关于std::atomic<int>的性能
在x86平台上,对齐的std::atomic<int>的普通加载/存储操作(使用默认或宽松内存顺序),汇编层面确实不会生成LOCK前缀,性能和普通对齐int的硬件操作几乎一致,但它同时提供了C++标准保证的原子性、可见性和有序性,完全没必要为微乎其微的性能差异(甚至可能不存在)牺牲代码的正确性和可移植性。
内容的提问来源于stack exchange,提问作者xmllmx

