现代C++中的内存序
内存顺序对比表
内存顺序 | 适用操作 | 同步保证 | 典型用途 |
|---|---|---|---|
| 任意 | 全局顺序一致性 | 默认选项,需要强保证时 |
| 加载(load) | 确保在该加载操作之后才读取非原子数据,以保证看到生产者线程Release操作之前的所有写入 | 读取发布数据(配合release) |
| 存储(store) | 保之前的所有内存操作(如数据写入)在该原子操作前完成 | 发布数据(配合acquire) |
| 读-修改-写 | 兼具acquire+release | 原子RMW操作 |
| 加载(load) | 依赖顺序(理论) | 不推荐使用 |
| 任意 | 仅原子性 | 计数器等无同步需求 |
1. std::memory_order_relaxed
最弱的内存序,仅保证原子性和修改顺序一致性,不提供同步和顺序约束。
示例1: 计数器
std::atomic<int> counter{0};// 多个线程执行 void increment_counter() { for (int i = 0; i < 1000; ++i) { counter.fetch_add(1, std::memory_order_relaxed); } }
// 读取计数器,可能读到中间值,但最终结果是准确的 int read_counter() { return counter.load(std::memory_order_relaxed); }
注意: 这个读取操作可能在任何时候返回计数器当前的值,但不与其他操作有顺序约束。
2. std::memory_order_consume (已弃用,不推荐使用)
本意是建立数据依赖关系,但实际中编译器几乎总是将其提升为memory_order_acquire。因此,我们跳过其示例,直接使用memory_order_acquire。
3. std::memory_order_acquire 和 std::memory_order_release
这对内存序用于建立同步关系:一个线程的释放操作与另一个线程的获取操作配对,使得释放操作之前的所有写操作(包括非原子写)对获取操作之后的读操作可见。
示例2: 通过原子标志位传递非原子数据
std::atomic<int*> data{nullptr}; std::atomic<bool> ready{false};// 线程1:生产者 void producer() { int* p = new int(42); // 先初始化非原子数据 *p = 42; // 释放操作:确保上面的写操作在释放之前完成,并且对获得这个标志为true的线程可见 data.store(p, std::memory_order_relaxed); // 可以用relaxed,因为下面有release ready.store(true, std::memory_order_release); // 释放:之前的写入对获取的线程可见 }
// 线程2:消费者 void consumer() { // 循环直到数据就绪 while (!ready.load(std::memory_order_acquire)) { // 等待 } // 到这里,ready的获取操作成功,线程1中release之前的所有写操作在此处都可见 int* p = data.load(std::memory_order_relaxed); assert(*p == 42); // 一定会成功 delete p; }
3.1 std::memory_order_acquire 和 std::memory_order_release的例子中,假如线程2 ready.load(std::memory_order_acquire)改成ready.load(std::memory_order_relax)会有问题吗?
假设重排:
在
consumer中:
读取
ready的值为true(但用的是relaxed,所以没有任何屏障阻止之后的读操作重排到之前)。读取
data得到指针p。读取
*p(即p指向的内存)。然而,由于编译器和处理器的优化,步骤1(
relaxed加载)不能阻止下面的乱序:
编译器和处理器可能把步骤3(
*p)的重排到步骤1(ready.load(relaxed))之前执行(只要在单线程内语义不变)。这意味着可能在ready还没有为true的时候就去读*p(但此时p还没有被设置? 注意我们还有data的读取,这里还需要进一步分析)。更准确的乱序可能性:
producer中的写操作可能被重排(但release会阻止重排到store(true)之后)。
consumer中,由于ready.load(relaxed)没有获得语义,它之后的任何读操作(包括data.load(relaxed)和*p)都可能被重排到它之前。但是,即使重排到它之前,在循环等待时我们只关心ready的值,所以不影响等待逻辑。但是最关键的是:即使读到了ready为true,却不能保证看到producer线程在release之前写入的数据(因为缺少acquire操作带来的可见性保证)。另一种错误:延迟的写入可见性
当
consumer线程执行ready.load(relaxed)并读到了true,但由于没有acquire语义,其他核心可能还没有看到producer线程在release之前对*p的写入(42)。所以,
consumer读取*p时可能读到旧值(比如内存中该地址尚未更新为42)或者初始值(比如0或垃圾值),导致断言失败。
4. std::memory_order_acq_rel
用于读-修改-写操作,同时具有获取和释放语义。即,它既同步之前的释放操作,又同步之后的获取操作。
示例3: 自旋锁实现
class SpinLock {
std::atomic_flag flag = ATOMIC_FLAG_INIT;
public:
void lock() {
while (flag.test_and_set(std::memory_order_acq_rel)) {
// 自旋
}
}
void unlock() {
flag.clear(std::memory_order_release);
}
};注意:test_and_set是读-修改-写操作,使用memory_order_acq_rel可以保证锁的获取操作(lock)具有获取语义,即在锁内操作之前,不能将锁内操作重排到加锁之前;而解锁操作(unlock)使用释放语义,锁内操作不能重排到解锁之后。同时,使用acq_rel可以保证多个线程对锁的竞争是公平的(修改顺序一致)。
但实际上,锁的获取通常需要获取语义,释放需要释放语义,所以这里memory_order_acquire和memory_order_release也可以,但acq_rel更准确地表达了读-修改-写同时具有两种语义。
5. std::memory_order_seq_cst
顺序一致性:默认选项,所有线程看到相同的操作顺序,且所有操作(包括非原子操作)不允许重排越过原子操作。
示例4: 顺序一致性的典型例子
std::atomic<bool> x = {false}; std::atomic<bool> y = {false}; std::atomic<int> z = {0};void write_x() { x.store(true, std::memory_order_seq_cst); // #1 }
void write_y() { y.store(true, std::memory_order_seq_cst); // #2 }
void read_x_then_y() { while (!x.load(std::memory_order_seq_cst)) {} // #3 if (y.load(std::memory_order_seq_cst)) { // #4 ++z; } }
void read_y_then_x() { while (!y.load(std::memory_order_seq_cst)) {} // #5 if (x.load(std::memory_order_seq_cst)) { // #6 ++z; } }
int main() { std::thread a(write_x); std::thread b(write_y); std::thread c(read_x_then_y); std::thread d(read_y_then_x); a.join(); b.join(); c.join(); d.join(); assert(z.load() != 0); // 可能为0吗? }
在顺序一致性下,所有操作(#1到#6)都按一个单一的全局顺序执行,且每个线程的操作顺序在全局顺序中保持不变。因此,在read_x_then_y中,看到x为真后,y一定为真(因为#1在#2之前发生)?但实际并非如此,因为线程a和b是并行的,两个写操作顺序未定。
然而,由于是顺序一致性,全局顺序中要么#1在#2之前,要么#2在#1之前。如果是#1在#2之前,那么read_y_then_x在循环退出(即看到#2为真)后,检查#1(x)也必定为真,因为#1在#2之前发生,所以#1已经被执行。反之亦然。因此,两个线程read_x_then_y和read_y_then_x中至少有一个会执行++z。所以assert(z.load() != 0)永远不会触发。
但是,如果使用更弱的内存序(例如release/acquire),则上述断言可能触发。因为弱内存序下,不同的线程可能看到不同的操作顺序,可能导致一个线程看到x为真而y仍为假,另一个线程看到y为真而x仍为假,那么两个if条件都不成立,z将保持0。
总结
relaxed:仅原子,无同步。release/acquire:配对使用,在两个线程之间建立同步关系。acq_rel:用于读-修改-写,同时具有获取和释放语义。seq_cst:全局顺序一致,最强约束,默认选项。
注意:实际编程中,应尽量使用默认的seq_cst,除非性能测试表明需要更弱的内存序。更弱的内存序虽然能提升性能,但正确性难以保证。
void incorrect_producer_relaxed() { data = 42; // 危险:使用relaxed存储,没有同步语义 ready.store(true, std::memory_order_relaxed); }
void correct_consumer() { while (!ready.load(std::memory_order_acquire)) ; // 使用acquire std::cout << data << std::endl; // 可能输出0,而不是42! }
#include <atomic> #include <thread> #include <iostream>std::atomic<bool> x{false}; std::atomic<bool> y{false}; std::atomic<int> z{0};
void write_x() { x.store(true, std::memory_order_seq_cst); }
void write_y() { y.store(true, std::memory_order_seq_cst); }
void read_x_then_y() { while (!x.load(std::memory_order_seq_cst)) ; if (y.load(std::memory_order_seq_cst)) ++z; }
void read_y_then_x() { while (!y.load(std::memory_order_seq_cst)) ; if (x.load(std::memory_order_seq_cst)) ++z; }
int main() { std::thread a(write_x); std::thread b(write_y); std::thread c(read_x_then_y); std::thread d(read_y_then_x); a.join(); b.join(); c.join(); d.join(); std::cout << "z = " << z << std::endl; }
请注意在上述例子中,还是有可能因为cache之间的同步延迟导致结果为0,即运行read_x_then_y线程cpu本地cache有y的旧值,运行read_y_then_x的线程cpu本地cache有x的旧值。seq_cst情况下,不会发生指令重排
暂无评论,欢迎留下第一条评论。