c++并发--原子操作,内存顺序,屏障

发布时间:2026/9/15 8:03:27
c++并发--原子操作,内存顺序,屏障 1.三个原子类型1.1.std::atomic_flag最简单、保证无锁is_lock_free() 恒为true的原子布尔标志。std::atomic_flag fATOMIC_FLAG_INIT;// 唯一合法初始化方式C20 前// C20 起可用 std::atomic_flag f; 默认初始化为 clear只有两个操作test()C20读、clear()写参数为memory_order不允许release/acq_rel/seq_cst以外——准确说clear只允许relaxed/release/seq_cst、test_and_set()RMW读-改-写。C20前没有test()所以它是一个纯测试并设置的标志位用于实现自旋锁class spinlock{std::atomic_flag flagATOMIC_FLAG_INIT;public:voidlock(){while(flag.test_and_set(std::memory_order_acquire)){}}voidunlock(){flag.clear(std::memory_order_release);}};acquire限制了lock后临界区的内容不会重排序到lock前release限制了unlock前临界区的内容不会重排到unlock后。不能拷贝、赋值、交换。1.2.std::atomicT 为整型/指针/浮点等 trivially copyable 类型通用原子封装操作load / store / exchange / compare_exchange_strong / weak / fetch_add / fetch_sub / fetch_and / fetch_or / fetch_xor / operator等。不可拷贝、不可赋值拷贝构造/拷贝赋值被删除但有一个非原子的operator(T)等价于store(r, seq_cst)。可能不是无锁的用锁实现可用is_lock_free()查询。1.3.std::atomicT*特化指针特化支持所有通用操作外加指针算术std::atomicint*p;p.fetch_add(3);// 前进 3 * sizeof(int)p.fetch_sub(1);// 后退p2;p-1;// 等价形式p;--p;// 自增自减fetch_add/fetch_sub是RMW操作返回修改前的旧指针。注意内存序约束指针运算只保证对指针值本身的原子性不保证指针所指对象的生命周期安全ABA问题的根源之一。2.五种基本操作与 memory_order 矩阵六种memory_order顺序归类relaxed无同步consumeload 类C26 已弃用/移除提案实践中等价 acquireacquireload类releasestore类acq_relRMW专用seq_cst全能默认且最强约束规则编译期强制store只允许relaxed / release / seq_cstload只允许relaxed / consume / acquire / seq_cstRMW允许全部六种传错顺序是UB运行期断言/编译期检查实现定义但标准上违规即未定义行为2.1.load 四种顺序Tload(memory_order orderseq_cst)constnoexcept;顺序语义relaxed仅保证读到某个时刻的真实值原子性无任何顺序保证。可读到任意时刻的值不与其他操作排序。consume保证对该原子变量依赖该读值的后续操作不被重排到读之前data dependency ordering。允许硬件上比 acquire 更便宜的优化如 ARM/PowerPC 上不需要内存屏障。注意编译器常把 consume 提升为 acquirestd::kill_dependency 也几乎无用C26 起 consume 被移除/弃用实践中直接用 acquire。acquire建立获取语义本线程中所有后续程序顺序在后的读写不能被重排到本次load之前。典型用途读取标志位后安全读取共享数据。seq_cstacquire全部语义 全局单一总顺序single total order。所有线程看到的seq_cst操作序列一致。最慢x86上load的seq_cst通常免费store 需要xchg/mfence。2.2.store 三种顺序voidstore(T desired,memory_order orderseq_cst)noexcept;顺序语义relaxed原子地写入其他线程不会读到撕裂值但无发布语义。前后代码可自由穿越这个 store 重排。release建立释放语义本线程中所有之前的读写不能被重排到本次store之后。与另一个线程的acquire-load形成happens-before配对使用。seq_cstrelease语义 参与全局总顺序。保证所有 seq_cst store 按同一顺序被所有线程观察到。释放-获取配对Dekker/Peterson式同步的基石// 线程 1 // 线程 2data42;if(flag.load(acquire)){flag.store(true,release);──▶// data 必为 42happens-before}2.3.exchangeRMWTexchange(T desired,memory_order orderseq_cst)noexcept;原子地写入新值并返回旧值一步到位。任何顺序都允许因为它同时是读和写只用relaxed当只需原子交换、不需要发布数据时用acq_rel若既要从旧值获取、又要向新值释放用seq_cst参与全局总顺序默认。2.4.compare_exchange_strong / weakCASboolcompare_exchange_strong(Texpected,T desired,memory_order success,memory_order failure)noexcept;boolcompare_exchange_strong(Texpected,T desired,memory_order orderseq_cst)noexcept;// failure order 但不得强于 successboolcompare_exchange_weak(同上);语义原子地执行if (current expected) { current desired; return true; } else { expected current; return false; }失败时expected会被改写为当前值——这是用CAS循环更新状态的机制。失败序必须是 load 类顺序relaxed/consume/acquire/seq_cst且不允许强于成功序。strong vs weak的区别strongweak假失败spurious failure不允许允许在某些平台如 ARM/LL-SC 架构上即使*this expected也可能返回 false开销可能更重需要内部循环保证更贴近硬件单条 LL/SC 指令使用场景只调用一次、不能容忍假失败写循环时必须包在while里// weak 必须这样用intoldhead.load(relaxed);while(!head.compare_exchange_weak(old,new_node,memory_order_release,memory_order_relaxed)){// old 已被刷新为最新值循环继续}无锁栈的push就依赖这个模式。3.RMW 六种顺序逐一解析RMWread-modify-write如fetch_add、exchange、CAS、x的语义 “读的顺序” 写的顺序的组合顺序读侧语义写侧语义典型场景relaxed无无纯计数器统计调用次数只需结果不丢更新consume数据依赖序—C 标准对 RMW 的 consume 写侧无意义罕见一般直接用 acquireacquire获取语义无读旧值以判断状态但不发布新数据release无释放语义发布新数据但不关心读到什么旧值acq_rel获取 释放同左同时拥有读到旧值后开始临界区acquire写入新值结束临界区release。无锁引用计数增减的经典选择seq_cst获取 全局总序释放 全局总序需要跨线程严格一致顺序时如 Dekker 算法互斥双方的两个 RMW// 引用计数 release 场景voidunref(){if(count.fetch_sub(1,std::memory_order_acq_rel)1)delete this;// 我是最后一个之前的 release 操作其他线程的 store}// 已同步可以安全析构关键点fetch_add(1, memory_order_release)的含义是本次加法结果对其他做过acquire的线程可见而不是加法操作之前的代码对任何线程有序——它是单边的。4.std::atomic 对用户自定义类型的要求C20 前 vs C20 起C11/14/17POD限制std::atomicT对自定义T的C17及之前要求是is_trivially_copyable_vT为true即可以用memcpy复制、无自定义拷贝构造/虚函数等可复制构造、可移动赋值std::memcpy 语义所有基类子对象和数据成员都满足上述条件不能是数组指针可以compare_exchange用bitwise比较memcmp语义而不是operator——因此不能有padding中的不确定值否则CAS可能永远失败padding字节每次拷贝不同。T必须平凡默认构造C17起要求is_default_constructible的平凡默认构造。需要atomicT特化可用否则操作全局std::atomicT模板的默认实现用内部锁is_lock_free()返回 false。C20起要求放宽std::atomicT的合法T变为任何is_trivially_copyable_vT且可平凡默认构造的类型——即union、含位域的结构体bit-field等也变得可用并且volatile限定语义整理。C20新增std::atomic_refT对已存在对象的原子引用不要求对象本身是atomic要求同样是trivially copyable。实践中自定义原子类型的正确姿势structConfig{inta;doubleb;charname[16];// 不能有 virtual、不能有自定义拷贝/析构// 注意对齐问题}__attribute__((aligned(8)));// 让 sizeof 对齐到 8 的倍数减少 padding 差异static_assert(std::is_trivially_copyableConfig::value,);std::atomicConfigcfg;padding陷阱structBad{charc;// 1 字节 7 字节 paddinginti;};std::atomicBadx;Bad expectedx.load();while(!x.compare_exchange_weak(expected,new_val)){}// 可能死循环padding 每次不同解决方案手工填充char pad[7]、alignas、或改用成员级原子变量。C26视角当前时间点std::atomic_flag的test() 已加入memory_order_consume和std::kill_dependency已被弃用实践中编译器把consume一律升级为acquire标准委员会放弃了修复它的努力atomicT的自定义类型要求保持不变trivially copyable。5.一张总表每种操作允许的顺序操作relaxedconsumeacquirereleaseacq_relseq_cststore✓✓✓load✓✓✓✓exchange✓✓✓✓✓✓compare_exchange_*✓✓✓✓✓✓fetch_add/sub/and/or/xor✓✓✓✓✓✓默认全是seq_cst——不写顺序就是最强的写无锁代码时这是最常见的性能bug来源x86上store的seq_cst比release多一条mfence/lock xchgARM上差距更大。6.选择建议实战计数器只统计不发布数据fetch_add(1, relaxed)。一次性初始化标志位store(true, release) load(acquire)比seq_cst的call_once更便宜。无锁队列/栈的push发布节点CAS用release或acq_rel失败侧relaxed。自旋锁test_and_set(acquire) / clear(release)。只有当你需要所有线程看到完全一致的交错顺序如实现Dekker互斥才用seq_cst。永远不要手动写consume直接acquire。7.内存顺序语义详细解释7.1.relaxed —— 唯一的规则是不撕裂前面的代码 ┆ 这个原子操作 ┆ 后面的代码 随便穿越随便换序只保证一件事这个读/写本身是原子的别的线程不会读到写了一半的值对int不会出现0x00001234和0x56780000拼出来的怪值。前后代码可以任意穿越它重排。比如x.store(1, relaxed)前后算好的普通变量别的线程读到 1 时完全可能还没算好。7.2.acquire —— “后面的不许跑到我前面”只约束后半边前面的代码 → 这个原子读 ║←屏障→║ 后面的代码 可以穿越下去 不许穿越上来规则本操作中之后的所有读写禁止重排到本操作之前。含义我获取到了某个信号读到了某个值那么读信号之后做的事逻辑上必须真的发生在读信号之后。7.3.release —— “前面的不许跑到我后面”只约束前半边前面的代码 ║←屏障→║ 这个原子写 → 后面的代码 不许穿越下去 可以穿越上来规则本操作之前的所有读写禁止重排到本操作之后。含义我把数据都算好、摆好了前面的代码然后释放出一个信号写入标志位。别人看到信号时数据必须已经摆好了。7.4.acq_rel —— 同时是 acquire 又是 release两头都堵前面的代码 ║←屏障→║ 读写 ║←屏障→║ 后面的代码 不许下去 不许上来前面不许穿下去 后面不许穿上来。关键它只约束本线程内的重排不约束所有线程看到的全局顺序——这是它和seq_cst的本质区别。7.5.seq_cst —— acq_rel 再加强制全球统一剧本拥有acq_rel的全部约束额外加一条所有线程的所有seq_cst操作在所有线程眼中都按同一个顺序发生。// 线程1 // 线程2a.store(1,seq_cst);b.store(1,seq_cst);r1b.load(seq_cst);r2a.load(seq_cst);7.6.结合硬件底层解释seq_cst与acq_rel每个核心里有一个store buffer写缓冲——一个几十项的小队列。执行x 1时核把x1放进自己的store buffer指令立刻退休流水线继续跑下一条缓冲里的写稍后才异步发给缓存系统真正变成全局可见可能滞后几十到几百个周期也就是说“指令执行了” ≠ “别的核能看见”。写有一个到达全球的时差。seq_cst的store在硬件上等于带全屏障的写x86lock前缀指令如lock xchg或mfence。作用排空本核整个写缓冲等所有先前的写真正进入缓存系统、拿到coherence point的确认store才退休。ARMstlr之外还需dmb ish级全屏障RMW形式的seq_cst常在指令后再补一道。效果体现在写的一端执行x.store(1, seq_cst)的核必须站在原地等x1全球落地才能执行下一条指令。cst下store操作既修改所在核的store buffer还需同步执行发送修改给缓存系统达成缓存一致性才继续下一条指令。这里还有几点需要注意核T上的写操作执行了异步发送但此异步发送被其他各个核所感知到的时间点也会因为核间距离不同而有所差异。但相同的源和目标下先发出的异步通知相比后发出的异步通知必然会被目标核先感知到。对写操作逻辑上通过release来担保过去。作为对比对读逻辑上通过acquire来担保未来。分别担保一半。8.操作原子语义操作读写为什么store(x)W只写入不管旧值是什么load()R只读取不改变值exchange(x)RW先读旧值再写入新值一步完成compare_exchange(e, d)RW读当前值来比较相等则写入fetch_add(1)RW读出旧值、写入旧值1为什么store行的空白格是acquire / consume / acq_relstore没有读部分。acquire的语义是我读到信号了后面的事不许提前——store什么都没读获取谁呢这个约束没有任何意义标准直接禁止用了就是 UB。release有意义“写之前的准备工作不许重排到写之后”。✓acq_rel acquire releaseacquire半边无意义 → 禁止。seq_cst有意义它含release语义 全球总序。✓为什么load行的空白格是release / acq_relload没有写部分。release的语义是我写出一个信号之前的准备必须已就位——load什么都没发布release无对象。acquire有意义“我读到信号了之后的消费不许提前”。✓seq_cst有意义含acquire 全球总序。✓为什么RMW行exchange / CAS / fetch_*六个全 ✓组合读侧约束写侧约束场景fetch_add(1, relaxed)无无纯计数只求不丢更新fetch_add(1, acquire)后面的不许提前无读到旧值后才开始干活但不对外发布什么fetch_add(1, release)无前面准备不许滞后算好东西写入新值即发布fetch_add(1, acq_rel)后面的不许提前前面准备不许滞后一次操作既进场又出场如引用计数减到 1 后析构fetch_add(1, seq_cst)以上全部 全球总序同上需要所有线程一致剧本8.std::atomic_thread_fence8.1.它是什么voidatomic_thread_fence(std::memory_order order)noexcept;// atomic一句话它只施加顺序约束本身不是任何读/写/RMW。编译器视角一道编译屏障——屏障两侧的内存操作不许互相跨越硬件视角一道CPU屏障——视顺序强度插入mfence / dmb等指令它不操作任何变量必须和通常relaxed的原子变量配合使用才有跨线程意义。它和给原子操作传memory_order的本质区别操作自带的顺序独立栅栏作用对象只约束那一个原子操作约束它两侧的所有内存操作位置焊死在操作上自由摆放——可以在循环外、函数边界、热路径之外典型用途常规同步把昂贵的序从热路径挪走做精细优化8.2.六种顺序的栅栏语义栅栏语义fence(relaxed)空操作只 compiler barrier 级别都算不上无跨线程效果fence(consume)数据依赖序已弃用同 acquirefence(acquire)之后的所有读写不许重排到栅栏之前fence(release)之前的所有读写不许重排到栅栏之后fence(acq_rel)两者兼有fence(seq_cst)acq_rel 参与全局总序 S栅栏是单边约束配对规则才是灵魂见下。8.3.核心配对规则栅栏怎么借力relaxed 原子操作栅栏自己不是原子操作无法被读到所以它需要找一个 relaxed 原子操作当载体线程 A 线程 B 普通写 x42;while(y.load(relaxed)!1){}fence(release);fence(acquire);y.store(1,relaxed);普通读 rx;// 保证 42和标准教科书例子的对照// 等价于 ready.store(1, release) ready.load(acquire)x42;// 数据准备std::atomic_thread_fence(std::memory_order_release);ready.store(true,std::memory_order_relaxed);// 信号载体// 对端while(!ready.load(std::memory_order_relaxed)){}std::atomic_thread_fence(std::memory_order_acquire);assert(x42);// 保证成立效果与release/acquire版完全等价区别只在约束的落点release版本把顺序焊在ready这一次操作上栅栏版本把顺序摊给了栅栏两侧的一切操作。8.4.一句话总结原子操作的memory_order把顺序焊在操作上atomic_thread_fence把顺序撒在两操作之间。前者够用、好读后者让你在多变量、热路径、批量同步的场景里以更低的成本精确铺设happens-before的边界。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询