You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

多线程counter++操作的原子性与内存可见性疑问解答

多线程计数器递增的原子性与内存架构疑问

示例代码

#include <thread>
#include <iostream>
#include <chrono>
#ifdef ATOMIC
#include <atomic>

std::atomic<int>
#else
int 
#endif    
counter(0);

void thread() {
    for(;;) {
#ifdef ATOMIC
        counter.fetch_add(1, std::memory_order_relaxed);
#else 
        counter++;
#endif
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

int main() {
    std::jthread t1{thread};
    std::jthread t2{thread};
    while (true) {
        std::cout << counter
#ifdef ATOMIC
        .load(std::memory_order_relaxed) 
#endif 
        << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(1000));
    }
}

背景与疑问

通常认为counter++包含「取当前值、递增、存回」三步,在多线程环境中会因原子性问题不符合预期。但观察编译后的汇编代码,counter++被译为单条指令add DWORD PTR counter[rip], 1,由此产生以下技术疑问:

  1. 为何单条汇编指令的counter++在多线程环境中仍不被视为原子操作?
  2. 内存可见性与缓存如何影响该操作?若计数器不在缓存中,是否会导致显著等待或指令被中断重试?

问题解答

1. 单条汇编指令的counter++为何不被视为原子操作?

  • 硬件原子性≠语言标准原子性:x86架构下,对齐的32位内存操作的add指令确实是硬件原子的,但这只是CPU的硬件特性,C标准并不承认普通int的++操作具有原子性。C标准规定,普通非原子变量的并发读写属于未定义行为,编译器完全可以无视多线程场景对代码做优化。
  • 编译器优化的不确定性:当开启编译优化(如-O2)时,编译器可能会将普通变量counter缓存到寄存器中,比如在循环里多次递增寄存器中的值,最后再一次性写回内存。此时实际执行的指令就不再是单条add,原子性自然无法保证。
  • 依赖硬件特性不可移植:即使在x86上能侥幸运行,换到其他架构(如ARM),单条内存add指令可能本身就不是原子的,程序会直接出现数据竞争问题。

2. 内存可见性与缓存的影响

  • 内存可见性问题:普通变量的读写没有内存屏障,线程对counter的修改可能仅停留在本地CPU缓存中,其他线程无法及时看到最新值,导致读取到过期数据。而std::atomic会通过指定的内存序(如memory_order_relaxed)保证修改的可见性,确保其他线程能看到最新的计数器值。
  • 缓存的影响:如果计数器不在当前CPU的缓存行中,CPU会通过MESI协议发起缓存同步请求,这个过程会有一定延迟,但不会导致指令中断重试。现代CPU会自动通过缓存锁定或总线锁定来保证原子操作的完成:比如x86的内存add指令在执行时,会锁定对应的缓存行,确保整个操作从读取到写回的过程不会被其他CPU打断。如果是未对齐的操作或超过CPU原生宽度的操作,可能会触发总线锁定,延迟会更高,但依然能保证原子性。不过对于普通变量,即使硬件处理了缓存同步,编译器的优化依然可能破坏可见性,导致线程间的数据不一致。

内容的提问来源于stack exchange,提问作者nowox

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.02 04:15:32