std::memory_order_relaxed
总结1
- 线程
A使用memory_order_relaxed写入x,不会与线程B对x的读取建立happens-before关系,因此无法保证线程B的读取一定能看到A写入的新值
本质
- 多核
CPU并不是所有核心在同一瞬间直接读写同一块内存 - 每个核心都有自己的执行流水线、
Store Buffer和多级缓存 - 一个核心完成了写操作,不代表另一个核心立即能观察到这个写操作
“写入完成”到底是什么意思
- 假设
|
1 |
std::atomic<int> x{0}; |
- 线程
A:
|
1 |
x.store(1, std::memory_order_relaxed); |
- 直觉上容易认为:
|
1 2 3 4 5 |
A 执行完 store ↓ 内存中的 x 立即变成 1 ↓ 所有 CPU 核心立即看到 1 |
- 但真实硬件更接近:
|
1 2 3 4 5 6 7 8 9 |
线程 A 执行 x = 1 ↓ 写入进入 CPU A 的 Store Buffer ↓ CPU A 可以继续执行后面的指令 ↓ 缓存一致性协议开始传播修改 ↓ CPU B 的缓存最终观察到新值 |
- 当
x = 1还在传播时,CPU B可能从自己的缓存层次中读取到旧值0
|
1 2 3 4 5 6 7 8 9 10 |
// 因此: // CPU A x.store(1, std::memory_order_relaxed); // CPU B int value = x.load(std::memory_order_relaxed); // CPU B 可能得到: value == 0; |
- 原子写入“完成”,首先可以表示它在
A核心本地完成并进入了存储系统,并不等于所有核心在同一时刻都已经观察到它
为什么需要 Store Buffer?
- 如果每个
store都必须等待所有CPU核心确认后才能继续执行,CPU会频繁停顿- 如果每次写入都要等缓存一致性协议完全处理完毕,处理器流水线会浪费大量时间
|
1 2 3 |
a = 1; b = 2; c = 3; |
- 所以现代处理器通常设置
Store BufferCPU把写操作放进Store Buffer后,就可以继续执行后续指令Store Buffer再在后台将写操作提交到缓存系统
|
1 |
CPU 执行单元 → Store Buffer → Cache → 缓存一致性系统 |
- 这大幅提高了性能,但也带来了一个结果
- 本核心认为写操作已经执行,其他核心却可能暂时还没看到它
原子性不等于立即可见性
- 这是理解
memory_order_relaxed最重要的一点 - 原子性解决的是
- 会不会看到一个被撕裂或损坏的中间值?
- 可见性与顺序解决的是
- 什么时候能够看到这个值,以及看到这个值时能否同时看到其他写入?
缓存一致性为什么没有自动解决
- 现代多核
CPU通常有缓存一致性协议,例如MESI一类的协议 - 它能保证各个核心不会对同一个缓存行永久持有互相矛盾的修改状态
- 但是缓存一致性协议的传播需要时间,并且不会自动等同于高级语言中的线程同步
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
CPU A CPU B x = 1 ↓ 进入 Store Buffer load x ↓ 暂时得到旧值 0 修改传播到一致性系统 ↓ CPU B 的旧缓存状态失效 ↓ 以后可能读到 1 |
memory_order_relaxed能做到的
- 其他线程不会读到“写了一半”的值,只会读到某个完整的合法值
- 但是这个完整的合法值可能是旧值,不是最新的
疑问
- 假如
A线程刚写,缓存一致性传播还没有完成,B线程读到的是旧值,B线程也开始写入,结果是,A线程和B线程都写了,最后只加了1,会不会发生这种情况?- 会发生
|
1 2 3 4 5 6 7 |
std::atomic<int> count{0}; void add_one() { int old = count.load(std::memory_order_relaxed); count.store(old + 1, std::memory_order_relaxed); } |
| 时刻 | 线程 A |
线程 B |
count |
1 |
读取 0 |
0 |
|
2 |
读取 0 |
0 |
|
3 |
写入 1 |
1 |
|
4 |
写入 1 |
1 |
- 两个线程都进行了“加一”,最终却只有:
|
1 |
count == 1; |
- 为什么原子变量还会丢失更新?
- 因为原子性只覆盖每一个单独操作:
- 但下面这整个过程不是一个原子操作:在读取和写回之间,其他线程可以介入
|
1 2 |
count.load(); // 单独原子 count.store(); // 单独原子 |
|
1 |
读取 → 计算 → 写回 |
解决
- 原子读—改—写
fetch_add是一个完整的原子读—改—写操作- 整个过程不可被其他线程插入
|
1 2 3 4 5 6 |
std::atomic<int> count{0}; void add_one() { count.fetch_add(1, std::memory_order_relaxed); } |
典型应用场景
- 判断标准是
- 是否只关心这个原子变量本身,而不需要通过它发布或保护其他数据?
- 答案是“是”,通常可以考虑
relaxed
- 统计计数器
|
1 2 3 4 5 6 |
std::atomic<std::uint64_t> request_count{0}; void process_request() { request_count.fetch_add(1, std::memory_order_relaxed); } |
- 生成唯一编号
|
1 2 3 4 5 6 |
std::atomic<std::uint64_t> next_id{0}; std::uint64_t generate_id() { return next_id.fetch_add(1, std::memory_order_relaxed); } |
- 不携带其他数据的状态标志
|
1 2 3 4 5 6 7 8 9 |
std::atomic<bool> stop{false}; // 控制线程 stop.store(true, std::memory_order_relaxed); // 工作线程 while (!stop.load(std::memory_order_relaxed)) { do_some_work(); } |
总结2
- 使用了
memory_order_relaxed- 如果只要求原子变量自身的读写完整,并且允许读取到旧值,可以使用
relaxed - 如果需要基于旧值原子地生成新值,避免多个线程覆盖更新,使用
fetch_add、fetch_sub - 如果需要通过原子变量同步、发布其他数据,不能只依靠
relaxed;通常需要release/acquire或互斥锁
- 如果只要求原子变量自身的读写完整,并且允许读取到旧值,可以使用
std::memory_order_release
核心语义
release操作之前的内存操作,不能以破坏发布语义的方式跑到release之后- 当另一个线程使用
acquire读取到这个release写入的值时,之前的数据也会对那个线程可见
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
#include <atomic> #include <iostream> #include <string> #include <thread> std::string data; std::atomic<bool> ready{false}; void producer() { data = "Hello"; // ① 准备普通数据 ready.store(true, std::memory_order_release); // ② 发布 } void consumer() { // ③ 获取发布状态 while (!ready.load(std::memory_order_acquire)) { } // ④ 可以安全读取 data std::cout << data << '\n'; } int main() { std::thread t1(producer); std::thread t2(consumer); t1.join(); t2.join(); } |
release 具体保证了什么
- 线程
A
|
1 2 3 4 |
data1 = 10; data2 = 20; ready.store(true, std::memory_order_release); |
release建立了一条“发布边界”:
|
1 2 3 4 |
data1 = 10 data2 = 20 ---------------- release 边界 ready = true |
- 编译器和
CPU必须保证:当另一个线程通过acquire成功观察到ready == true时,也能观察到 release 之前的:- 它并不是简单地“立即刷新所有缓存”,而是
C++对最终可观察结果作出的顺序保证 - 编译器会根据处理器架构生成必要的约束或指令
- 它并不是简单地“立即刷新所有缓存”,而是
|
1 2 |
data1 = 10; data2 = 20; |
release 必须配合 acquire 才能传递数据
- 只有生产者写
release:
|
1 2 3 |
if (ready.load(std::memory_order_relaxed)) { use(data); // 没有 acquire,不能依靠它安全读取 data } |
- 消费者使用
acquire
|
1 2 3 |
if (ready.load(std::memory_order_acquire)) { use(data); } |
release 通常用于 store
release和acquire
|
1 2 3 |
flag.store(true, std::memory_order_release); flag.load(std::memory_order_acquire); |
- 原子读—改—写操作也可以具有
release语义
|
1 |
count.fetch_add(1, std::memory_order_release); |
- 但如果这个
RMW操作既要获取之前的数据,又要发布后面的状态,通常使用
|
1 |
count.fetch_add(1, std::memory_order_acq_rel); |
std::memory_order_seq_cst
概述
C++中最强、最容易推理的原子内存顺序sequentially consistent
核心模型
- 所有
seq_cst原子操作,看起来像是按照某一个全局统一的顺序依次发生,并且这个全局顺序必须与各线程内部的代码顺序一致
默认就是 seq_cst
- 如果不明确指定内存顺序
|
1 2 3 4 5 6 |
std::atomic<int> x{0}; x.store(1); int value = x.load(); x.fetch_add(1); ++x; |
|
1 2 3 4 5 6 7 8 |
// 默认等价于 x.store(1, std::memory_order_seq_cst); int value = x.load(std::memory_order_seq_cst); x.fetch_add(1, std::memory_order_seq_cst); |
seq_cst 提供哪些保证
- 单次原子操作不可分割
|
1 |
x.store(1, std::memory_order_seq_cst); |
acquire/release语义- 对于
store:它至少具有release效果,可以发布之前的数据 - 对于
load:它至少具有acquire效果,可以获取相应store发布的数据 - 对于
RMW:同时具有acquire和release的效果
- 对于
|
1 2 3 |
x.store(1, std::memory_order_seq_cst); x.fetch_add(1, std::memory_order_seq_cst); |
- 所有
seq_cst操作具有统一的全局顺序- 多个线程操作
x和y时,所有seq_cst操作都必须能够排列到一个统一的顺序中
- 多个线程操作
|
1 2 |
std::atomic<int> x{0}; std::atomic<int> y{0}; |
Store Buffering示例
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
std::atomic<int> x{0}; std::atomic<int> y{0}; int r1 = -1; int r2 = -1; // 线程 A x.store(1, std::memory_order_relaxed); r1 = y.load(std::memory_order_relaxed); // 线程 B y.store(1, std::memory_order_relaxed); r2 = x.load(std::memory_order_relaxed); // 使用 relaxed 时 // r1 == 0 && r2 == 0 是允许的 |
|
1 2 3 4 5 6 7 |
// 线程 A x.store(1, std::memory_order_seq_cst); // A1 r1 = y.load(std::memory_order_seq_cst); // A2 // 线程 B y.store(1, std::memory_order_seq_cst); // B1 r2 = x.load(std::memory_order_seq_cst); // B2 |
|
1 2 3 4 |
// 线程内部顺序要求 A1 < A2 B1 < B2 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// 如果 r1 == 0; // 说明 A2 在全局顺序中没有观察到 B1,可以理解为: A2 < B1 // r2 == 0; // 说明 B2 在全局顺序中没有观察到 A1,可以理解为: B2 < A1 // 组合所有关系 // A1 < A2 < B1 < B2 < A1 // 形成循环,不可能建立合法的全局顺序 // 所以使用 seq_cst 后 // r1 == 0 && r2 == 0 不允许出现,其他三个结果仍然可能 |
r1 |
r2 |
是否可能 |
| 0 | 0 | 不可能 |
| 0 | 1 | 可能 |
| 1 | 0 | 可能 |
| 1 | 1 | 可能 |
-
也就是说,
seq_cst仍然可能读到旧值seq_cst并不意味着每次load都必须读到“现实时间上最新”的值
-
总结,
seq_cst保证的是- 所有操作能够被解释为一个统一且不矛盾的全局顺序
- 但它并不保证线程
B的读取一定排在线程A的写入之后
总结
seq_cst真正额外保证的是- 所有线程中的全部
seq_cst原子操作,可以放进一个所有线程都认可的全局总顺序中,并且这个全局顺序不能违反各线程内部的代码顺序
- 所有线程中的全部
- 示例
|
1 2 3 4 5 6 7 |
// 线程 A x.store(1, std::memory_order_seq_cst); // A1 y.load(std::memory_order_seq_cst); // A2 // 线程 B y.store(1, std::memory_order_seq_cst); // B1 x.load(std::memory_order_seq_cst); // B2 |
- 首先,各线程内部必须满足:
|
1 2 |
A1 < A2 B1 < B2 |
- 除此之外,所有操作还要形成一个统一的全局顺序,例如:
- 具体是哪一种不确定,但必须存在一种不矛盾的顺序,并且所有线程都遵守它
|
1 2 3 4 5 |
A1 < B1 < A2 < B2 // 或 B1 < A1 < B2 < A2 |
谁决定了最终是哪一种顺序
- 没有单一决定者,它是以下因素共同造成的
|
1 2 3 4 5 6 |
操作系统线程调度 + 编译器生成的机器指令 + CPU 的实际执行过程 + Store Buffer + 缓存一致性协议 + 内存屏障和原子指令 |
std::memory_order_consume
结论
- 使用
memory_order_acquire代替
失败
- 问题主要不在
CPU,而在编译器 - 编译器优化可能改变依赖关系
std::memory_order_acquire
acquire一旦读到了release发布的值,就保证当前线程能够看到对应release之前完成的所有内存操作
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
#include <atomic> #include <iostream> #include <thread> int data = 0; std::atomic<bool> ready{false}; void producer() { data = 42; ready.store( true, std::memory_order_release); } void consumer() { while (!ready.load( std::memory_order_acquire)) { } std::cout << data << '\n'; } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
class appcenter { public: appcenter_helper_file* get_helper(delegate* app) { appcenter_helper_file* temp = m_helper.load(std::memory_order_acquire); if (temp == nullptr) { std::lock_guard<std::mutex> lock(m_mutex); temp = m_helper.load(std::memory_order_relaxed); if (temp == nullptr) { temp = new appcenter_helper_file(app); m_helper.store( temp, std::memory_order_release); } } return temp; } private: std::atomic<appcenter_helper_file*> m_helper{nullptr}; std::mutex m_mutex; }; |
std::memory_order_acq_rel
一句话理解
- 当前原子操作既要获取其他线程之前发布的数据,又要把本线程之前的数据发布给后续线程
- 它主要用于原子读—改—写(
RMW)操作:
|
1 2 3 4 5 |
exchange fetch_add fetch_sub compare_exchange_weak compare_exchange_strong |
两个方向的作用
- 假设有一个
acq_rel原子操作
|
1 2 3 |
atomic_value.fetch_add( 1, std::memory_order_acq_rel); |
|
1 2 3 4 5 6 7 8 9 10 11 |
// 可以把它理解为一道双向同步点 其他线程通过 release 发布的数据 ↓ acquire:获取进来 ↓ 当前 RMW 操作 ↓ release:发布出去 ↓ 后续线程通过 acquire 获取数据 |
- 两个方向承担的职责不同
acquire部分:获取当前RMW所观察到的release发布的数据release部分:发布本线程在当前RMW之前完成的数据
|
1 2 3 |
std::atomic<int> state{0}; int data_a = 0; int data_b = 0; |
|
1 2 3 4 5 6 7 |
// 线程A data_a = 42; state.store( 1, std::memory_order_release); |
|
1 2 3 4 5 6 7 8 9 10 11 |
// 线程B data_b = 100; int old = state.fetch_add( 1, std::memory_order_acq_rel); // acquire 部分: // 如果 old == 1,并且读到了 A 的 release store, // 那么这里可以看到 data_a == 42。 |
|
1 2 3 4 5 6 7 |
// 线程C int value = state.load(std::memory_order_acquire); // 如果读到了 B 的 RMW 发布的值, // 就可以获取 B 在 RMW 前发布的 data_b = 100。 |
- 同步链可以理解为
- 线程
B的RMW: acquire部分从线程A获取data_arelease部分向线程C发布data_bfetch_add自身保证读取、加一、写回不可分割
- 线程
|
1 2 3 4 5 6 |
线程 A 线程 B 线程 C data_a = 42 data_b = 100 ↓ ↓ store(1, release) → fetch_add(1, acq_rel) → load(acquire) acquire + release |
声明:本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ Protobuf记述与使用01/24
- ♥ glog记述:概述使用06/30
- ♥ C++11_第五篇12/08
- ♥ 文件md5值计算05/31
- ♥ C++并发编程 _ 共享数据05/16
- ♥ COM组件_303/07