• 忘掉天地
  • 仿佛也想不起自己
bingliaolongBingliaolong  2026-07-29 00:05 Aet 隐藏边栏 |   抢沙发  1 
文章评分 1 次,平均分 5.0

std::memory_order_relaxed

总结1

  1. 线程 A 使用 memory_order_relaxed 写入 x,不会与线程 Bx 的读取建立 happens-before 关系,因此无法保证线程 B 的读取一定能看到 A 写入的新值

本质

  1. 多核 CPU 并不是所有核心在同一瞬间直接读写同一块内存
  2. 每个核心都有自己的执行流水线、Store Buffer 和多级缓存
  3. 一个核心完成了写操作,不代表另一个核心立即能观察到这个写操作

“写入完成”到底是什么意思

  1. 假设

  1. 线程 A

  1. 直觉上容易认为:

  1. 但真实硬件更接近:

  1. x = 1 还在传播时,CPU B 可能从自己的缓存层次中读取到旧值 0

  1. 原子写入“完成”,首先可以表示它在 A 核心本地完成并进入了存储系统,并不等于所有核心在同一时刻都已经观察到它

为什么需要 Store Buffer

  1. 如果每个 store 都必须等待所有 CPU 核心确认后才能继续执行,CPU 会频繁停顿
    1. 如果每次写入都要等缓存一致性协议完全处理完毕,处理器流水线会浪费大量时间

  1. 所以现代处理器通常设置 Store Buffer
    1. CPU 把写操作放进 Store Buffer 后,就可以继续执行后续指令
    2. Store Buffer 再在后台将写操作提交到缓存系统

  1. 这大幅提高了性能,但也带来了一个结果
    1. 本核心认为写操作已经执行,其他核心却可能暂时还没看到它

原子性不等于立即可见性

  1. 这是理解 memory_order_relaxed 最重要的一点
  2. 原子性解决的是
    1. 会不会看到一个被撕裂或损坏的中间值?
  3. 可见性与顺序解决的是
    1. 什么时候能够看到这个值,以及看到这个值时能否同时看到其他写入?

缓存一致性为什么没有自动解决

  1. 现代多核 CPU 通常有缓存一致性协议,例如 MESI 一类的协议
  2. 它能保证各个核心不会对同一个缓存行永久持有互相矛盾的修改状态
    1. 但是缓存一致性协议的传播需要时间,并且不会自动等同于高级语言中的线程同步

memory_order_relaxed能做到的

  1. 其他线程不会读到“写了一半”的值,只会读到某个完整的合法值
  2. 但是这个完整的合法值可能是旧值,不是最新的

疑问

  1. 假如A线程刚写,缓存一致性传播还没有完成,B线程读到的是旧值,B线程也开始写入,结果是,A线程和B线程都写了,最后只加了1,会不会发生这种情况?
    1. 会发生

时刻 线程 A 线程 B count
1 读取 0 0
2 读取 0 0
3 写入 1 1
4 写入 1 1
  1. 两个线程都进行了“加一”,最终却只有:

  1. 为什么原子变量还会丢失更新?
    1. 因为原子性只覆盖每一个单独操作:
    2. 但下面这整个过程不是一个原子操作:在读取和写回之间,其他线程可以介入

解决

  1. 原子读—改—写
  2. fetch_add 是一个完整的原子读—改—写操作
  3. 整个过程不可被其他线程插入

典型应用场景

  1. 判断标准是
    1. 是否只关心这个原子变量本身,而不需要通过它发布或保护其他数据?
    2. 答案是“是”,通常可以考虑 relaxed
  2. 统计计数器

  1. 生成唯一编号

  1. 不携带其他数据的状态标志

总结2

  1. 使用了memory_order_relaxed
    1. 如果只要求原子变量自身的读写完整,并且允许读取到旧值,可以使用 relaxed
    2. 如果需要基于旧值原子地生成新值,避免多个线程覆盖更新,使用 fetch_addfetch_sub
    3. 如果需要通过原子变量同步、发布其他数据,不能只依靠 relaxed;通常需要 release/acquire 或互斥锁

std::memory_order_release

核心语义

  1. release 操作之前的内存操作,不能以破坏发布语义的方式跑到 release 之后
  2. 当另一个线程使用 acquire 读取到这个 release 写入的值时,之前的数据也会对那个线程可见

release 具体保证了什么

  1. 线程 A

  1. release 建立了一条“发布边界”:

  1. 编译器和 CPU 必须保证:当另一个线程通过 acquire 成功观察到 ready == true 时,也能观察到 release 之前的:
    1. 它并不是简单地“立即刷新所有缓存”,而是 C++ 对最终可观察结果作出的顺序保证
    2. 编译器会根据处理器架构生成必要的约束或指令

release 必须配合 acquire 才能传递数据

  1. 只有生产者写 release

  1. 消费者使用acquire

release 通常用于 store

  1. releaseacquire

  1. 原子读—改—写操作也可以具有 release 语义

  1. 但如果这个 RMW 操作既要获取之前的数据,又要发布后面的状态,通常使用

std::memory_order_seq_cst

概述

  1. C++ 中最强、最容易推理的原子内存顺序
  2. sequentially consistent

核心模型

  1. 所有 seq_cst 原子操作,看起来像是按照某一个全局统一的顺序依次发生,并且这个全局顺序必须与各线程内部的代码顺序一致

默认就是 seq_cst

  1. 如果不明确指定内存顺序

seq_cst 提供哪些保证

  1. 单次原子操作不可分割

  1. acquire/release 语义
    1. 对于 store:它至少具有 release 效果,可以发布之前的数据
    2. 对于 load:它至少具有 acquire 效果,可以获取相应 store 发布的数据
    3. 对于 RMW:同时具有 acquirerelease 的效果

  1. 所有 seq_cst 操作具有统一的全局顺序
    1. 多个线程操作 xy 时,所有 seq_cst 操作都必须能够排列到一个统一的顺序中

  1. Store Buffering 示例

r1 r2 是否可能
0 0 不可能
0 1 可能
1 0 可能
1 1 可能
  1. 也就是说,seq_cst 仍然可能读到旧值

    1. seq_cst 并不意味着每次 load 都必须读到“现实时间上最新”的值
  2. 总结,seq_cst 保证的是

    1. 所有操作能够被解释为一个统一且不矛盾的全局顺序
    2. 但它并不保证线程 B 的读取一定排在线程 A 的写入之后

总结

  1. seq_cst 真正额外保证的是
    1. 所有线程中的全部 seq_cst 原子操作,可以放进一个所有线程都认可的全局总顺序中,并且这个全局顺序不能违反各线程内部的代码顺序
  2. 示例

  1. 首先,各线程内部必须满足:

  1. 除此之外,所有操作还要形成一个统一的全局顺序,例如:
    1. 具体是哪一种不确定,但必须存在一种不矛盾的顺序,并且所有线程都遵守它

谁决定了最终是哪一种顺序

  1. 没有单一决定者,它是以下因素共同造成的

std::memory_order_consume

结论

  1. 使用memory_order_acquire代替

失败

  1. 问题主要不在 CPU,而在编译器
  2. 编译器优化可能改变依赖关系

std::memory_order_acquire

  1. acquire 一旦读到了 release 发布的值,就保证当前线程能够看到对应 release 之前完成的所有内存操作

std::memory_order_acq_rel

一句话理解

  1. 当前原子操作既要获取其他线程之前发布的数据,又要把本线程之前的数据发布给后续线程
  2. 它主要用于原子读—改—写(RMW)操作:

两个方向的作用

  1. 假设有一个 acq_rel 原子操作

  1. 两个方向承担的职责不同
    1. acquire 部分:获取当前 RMW 所观察到的 release 发布的数据
    2. release 部分:发布本线程在当前 RMW 之前完成的数据

  1. 同步链可以理解为
    1. 线程 BRMW:
    2. acquire 部分从线程 A 获取 data_a
    3. release 部分向线程 C 发布 data_b
    4. fetch_add 自身保证读取、加一、写回不可分割

声明:本文为原创文章,版权归所有,欢迎分享本文,转载请保留出处!

bingliaolong
Bingliaolong 关注:0    粉丝:0
Everything will be better.

发表评论

表情 格式 链接 私密 签到
扫一扫二维码分享