资讯详情

资讯详情

C++ 原子操作的内存序:为什么需要它

本文深入解析std::atomic的三大核心属性原子性、可见性与顺序性强调仅用atomic不等于线程安全。内存序是解决可见性与顺序性的关键通过happens-before语义屏蔽编译器与CPU重排差异。六种内存序按强度分层从relaxed到seq_cst需根据场景选择默认用seq_cst保证跨平台安全发布-订阅用release/acquire计数器可用relaxed。volatile不能替代atomic。硬件层面x86的TSO模型使release/acquire开销小而ARM等弱模型需屏障指令导致同一代码在不同平台行为迥异。核心观点内存序不是性能开关而是确保多线程间状态传递正确性的语言级保障。一、问题起点原子性不等于可见性也不等于顺序性std::atomic解决三件不同的事原子性读-改-写不会被拆开不会出现“撕裂值”。可见性一个线程的写入什么时候能被另一个线程看到。顺序性多个内存操作之间的先后关系其他线程是否也能观察到同样顺序。很多人以为“用了std::atomic就线程安全了”这是错的。原子性只解决第 1 点。内存序解决的是第 2、3 点。二、为什么需要内存序两层重排没有内存序约束时两件事会重排1. 编译器重排data 42; ready 1;编译器可以变成ready 1; data 42;只要单线程执行结果不变编译器就有权这么做。多线程下另一个线程可能先看到ready 1再读data得到 0。2. CPU 重排现代 CPU 是流水线、乱序执行、写缓冲store buffer的。x86 是 TSOTotal Store OrderLoad 不重排 LoadStore 不重排 Store只允许 Store → Load 重排ARM / RISC-V / POWER 是弱内存模型Load-Load、Load-Store、Store-Store、Store-Load 都可能重排所以同一份 C 代码x86 上“碰巧对”ARM 上直接出数据竞争内存序就是用来屏蔽这些差异的语言级抽象。三、happens-before内存序的真正语义C不让你去想“CPU 先执行了哪条指令”而是定义如果 Ahappens-before​ B那么 A 的副作用对执行 B 的线程一定可见。happens-before 来自同一线程内的程序顺序release写 与 同一原子变量上的acquire读 之间的 synchronizes-withmutex::unlock→mutex::lockthread启动、join、条件变量通知等内存序的作用就是告诉编译器和 CPU“在这一点前后哪些普通内存访问不许跨过来。”四、六种内存序按强度分层1.memory_order_relaxed只保证原子性不保证顺序。counter.fetch_add(1, std::memory_order_relaxed);适用引用计数自增统计计数器不依赖“其他变量也同时可见”的场景不适用用原子变量做“数据发布”生产者-消费者握手2.memory_order_acquire读端while (!ready.load(std::memory_order_acquire)) {} // 后面读 data 一定看到生产者释放前的内容 int x data;语义本次 load 之后的读写不能被重排到本次 load 之前如果读到了别人 release 写入的值就建立 synchronizes-with3.memory_order_release写端data 42; ready.store(true, std::memory_order_release);语义本次 store 之前的读写不能被重排到本次 store 之后消费者若 acquire 到这个值就能看到data 42经典发布模型// 生产者 payload ...; flag.store(true, release); // 消费者 while (!flag.load(acquire)) ; use(payload); // 安全4.memory_order_acq_rel用于 read-modify-writeval.fetch_add(1, std::memory_order_acq_rel);既是 acquire 又是 release。常用于自旋锁无锁栈/队列里的 CAS/RMW5.memory_order_seq_cst默认内存序最强有 acquire/release 语义所有 seq_cst 操作之间存在一个全局总顺序所有线程看到这些原子操作的顺序完全一致x.store(1, std::memory_order_seq_cst); y.store(1, std::memory_order_seq_cst);其他线程不会看到“矛盾的全序视角”。代价x86 上可能生成mfence或xchgARM 上需要dmb ish多核高竞争时性能明显下降6.memory_order_consume基于数据依赖的弱同步。C26起弃用主流编译器直接按 acquire 处理实际工程基本不用。五、为什么不能用“volatile”代替volatile只阻止编译器优化不保证CPU 不重排多核可见顺序与其他变量的顺序关系volatile不是并发原语。std::atomic 正确内存序才是。六、硬件映射同一份 C不同汇编x86-64ready.store(true, memory_order_release); // 通常就是 mov [ready], 1 // 不需要 mfence flag.load(memory_order_acquire); // 通常就是 mov rax, [flag]x86 TSO 已经隐含了 StoreStore / LoadLoad 顺序所以 release/acquire 在 x86 上“几乎免费”主要约束编译器。ready.store(true, memory_order_seq_cst); // 可能生成 // mov [ready], 1 // mfence // 或 xchg [ready], regARM64ready.store(true, release); // stlr flag.load(acquire); // ldar seq_cst store; // dmb ish storeARM 允许大量重排所以 release/acquire 会真正变成屏障指令。这就是“x86 上没问题ARM 上一炸再炸”的根本原因。七、典型错误消息传递用 relaxed// 错误 data 42; ready.store(true, memory_order_relaxed); // 另一线程 if (ready.load(memory_order_relaxed)) { assert(data 42); // 可能失败 }relaxed 不建立 happens-before。ready true被看到时data的写入可能还没传播出去。正确写法data 42; ready.store(true, memory_order_release); if (ready.load(memory_order_acquire)) { assert(data 42); // 安全 }八、选型原则默认用 seq_cst​正确、可推理、跨平台安全。发布数据用 release / acquire​无锁队列、SPSC 管道、标志位握手的首选。独立计数器用 relaxed​不发布其他状态只求自增原子性。不要过早优化内存序​大多数无锁 bug 来自“我以为 relaxed 够用”。多个变量要一起可见用 mutex 或把数据放进指针后 release 指针​原子变量只能保证“这一个对象”的原子性不能自动保护一堆普通变量。九、一句话本质内存序不是“让程序更快的开关”而是告诉编译器和 CPU“哪些内存操作必须像单线程里那样保持先后关系和跨线程可见性。”没有内存序原子变量只是“不会撕裂的变量”有了内存序它才成为线程之间传递状态的通道。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →