现代C++中的内存序
内存顺序对比表
| 内存顺序 | 适用操作 | 同步保证 | 典型用途 |
|---|---|---|---|
| seq_cst | 任意 | 全局顺序一致性 | 默认选项,需要强保证时 |
| acquire | 加载(load) | 确保在该加载操作之后才读取非原子数据,以保证看到生产者线程Release操作之前的所有写入 | 读取发布数据(配合release) |
| release | 存储(store) | 保之前的所有内存操作(如数据写入)在该原子操作前完成 | 发布数据(配合acquire) |
| acq_rel | 读-修改-写 | 兼具acquire+release | 原子RMW操作 |
| consume | 加载(load) | 依赖顺序(理论) | 不推荐使用 |
| relaxed | 任意 | 仅原子性 | 计数器等无同步需求 |
1. std::memory_order_relaxed
最弱的内存序,仅保证原子性和修改顺序一致性,不提供同步和顺序约束。
示例1: 计数器
std::atomic<int> counter;
}
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: 自旋锁实现
在顺序一致性下,所有操作(#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情况下,不会发生指令重排
暂无评论,欢迎留下第一条评论。