75 / 80 · C++20 · 约 13 分钟
内存序:relaxed、acquire-release 与 happens-before
内存序描述原子操作如何约束周围访问。relaxed 保留原子性却不发布普通数据;读取到对应 release 写入的 acquire 才建立跨线程同步。判断正确性要画出先行发生链,不能依赖机器上看似稳定的执行顺序。
先行发生是证明关系,不是墙上时间
同一线程内的规定求值顺序称为 sequenced-before;合适的跨线程同步操作建立 synchronizes-with。把这些边连接并传递,就能证明相关写入 happens-before 后续读取。仅仅看到线程甲似乎先执行完、或者让乙睡眠一会儿,并不建立这样的关系。
每个原子对象都有自己的修改顺序,即使使用 relaxed 也是如此;但多个对象的修改顺序并不因此合成全局顺序。relaxed 适合独立计数等只关心原子值的场景,不适合单独拿一个标志宣布普通对象已经初始化。标志读取合法,不意味着旁边的数据读取也合法。
一次发布的完整证明
示例中生产者先写普通对象 payload,再对 ready 执行 release 存储。消费者通过 acquire 等待观察到 ready 从 false 变成 true,然后读取 payload。因为唯一写入 true 的操作就是该 release,成功观察值的 acquire 与它同步;写 payload → release → acquire → 读 payload 构成完整先行发生链。
因此 payload 不必自身是原子类型。关键是发布后不再修改它,而且对象必须一直存活。代码刻意在 join 之前读取 payload,读取的安全性来自发布协议,不是事后的 join。C++20 的 atomic::wait 会检查当前值,通知即使先发生也不会丢掉已经改变的状态;notify_one 用来唤醒,不是发布数据的内存屏障。
选择足够强且能解释的顺序
默认的 seq_cst 在相应 acquire/release 效果之外,为所有 seq_cst 操作提供一个受标准约束的单一全序,适合初始实现和需要跨原子对象推理的算法。它不会把任意普通访问自动变安全,也不会把多个操作合成事务。acq_rel 通常用于同时接收旧状态并发布新状态的读改写操作。
不要机械地给所有读取标 acquire、所有写入标 release 就宣布安全:读取必须读到相关 release 写入或规则允许的 release sequence 中的值。示例只做一次发布,不把 ready 重置为 false;反复复用缓冲区需要消费者确认读取结束,避免生产者下一轮写入与当前读取竞争。优化到 relaxed 前,应明确删掉的是哪条同步边,并证明剩余边仍足够。
容易答错的地方
- 把示例的 store 或 wait 改成 relaxed 会移除所需同步关系;普通 payload 读取不再有正确性保证,不只是偶尔读到旧值。
- atomic::wait 可能错过旧值变成新值又变回旧值的瞬间变化;一次发布不复位避免了这种 ABA 风险,循环通信必须另行设计。
运行一个例子
最低标准 C++20 · 完整程序 · 下载 .cpp
#include <atomic>
#include <cassert>
#include <iostream>
#include <thread>
struct Payload {
int left = 0;
int right = 0;
};
int main() {
Payload payload;
std::atomic<bool> ready{false};
std::jthread producer([&] {
payload.left = 6;
payload.right = 7;
ready.store(true, std::memory_order_release);
ready.notify_one();
});
ready.wait(false, std::memory_order_acquire);
const int result = payload.left * payload.right;
assert(payload.left == 6 && payload.right == 7);
assert(result == 42);
producer.join();
std::cout << result << '\n';
}
在本地编译
g++ -std=c++20 -Wall -Wextra -Wpedantic -pthread concurrency-memory-order.cpp -o example && ./example预期结果
42
CHECK YOUR UNDERSTANDING
合上答案,试着解释。
若消费者先 producer.join(),再以 relaxed 读取 ready 和 payload,是否安全?这能证明原来的发布协议可以使用 relaxed 吗?
查看参考答案
先成功 join 后读取是安全的:生产者完成与 join 返回同步,生产者的 payload 写入先行发生于之后读取,且没有后续写入。但这不能证明原协议可用 relaxed,因为加入 join 换了一条同步路径。原例要求消费者在 join 前使用数据,此时仍必须保留 release/acquire 发布链。
继续查证
- cppreference:memory_order 的修改顺序与 release-acquire
- cppreference:atomic::wait 的值检查与 ABA 注意事项
- C++ 工作草案:[intro.races] happens-before 与 synchronizes-with
标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。