内存顺序对比表
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情况下,不会发生指令重排