详解 C++ 的内存序模型
C++ 提供了内存序模型来控制多线程程序中不同线程对共享内存的访问顺序。最常用的是顺序一致性内存模型(memory_order_seq_cst
),但它也提供了其他模型(如 memory_order_relaxed
)以优化性能。
一、顺序一致性内存模型(memory_order_seq_cst
)
定义
顺序一致性内存模型保证多线程程序中的所有内存操作以全局一致的顺序执行。这种模型使程序的行为更接近单线程程序的直觉。
特点
-
全局操作顺序
- 所有线程中的所有原子操作都按照某个全局一致的顺序执行。
- 不同线程观察到的原子操作顺序相同。
-
同步可预测
- 线程对共享数据的操作结果可预测,符合程序员的直觉。
-
开销
- 是最严格的内存模型,硬件需要更多的开销(如 CPU 指令重排序限制和内存屏障)。
示例代码
#include <atomic>
#include <iostream>
#include <thread>
std::atomic<int> a{0};
std::atomic<int> b{0};
void thread1() {
a.store(1, std::memory_order_seq_cst); // 顺序一致地存储
std::cout << "Thread 1: " << b.load(std::memory_order_seq_cst) << std::endl; // 顺序一致地加载
}
void thread2() {
b.store(1, std::memory_order_seq_cst);
std::cout << "Thread 2: " << a.load(std::memory_order_seq_cst) << std::endl;
}
int main() {
std::thread t1(thread1);
std::thread t2(thread2);
t1.join();
t2.join();
return 0;
}
输出行为
无论硬件如何优化,由于使用了 memory_order_seq_cst
,两个线程的操作将按照全局一致的顺序执行。例如:
Thread 1: 0
Thread 2: 1
二、其他内存序
1. memory_order_relaxed
定义
- 最弱的内存序模型,不保证操作的顺序。
- 只保证当前操作的原子性。
特点
-
无同步
- 不提供线程间的同步,操作可能被重新排序。
- 不适合用于需要线程间通信的场景。
-
高性能
- 性能最好,因为没有内存屏障和指令重排序限制。
示例代码
#include <atomic>
#include <iostream>
#include <thread>
std::atomic<int> x{0};
std::atomic<int> y{0};
int r1 = 0, r2 = 0;
void thread1() {
x.store(1, std::memory_order_relaxed);
r1 = y.load(std::memory_order_relaxed);
}
void thread2() {
y.store(1, std::memory_order_relaxed);
r2 = x.load(std::memory_order_relaxed);
}
int main() {
std::thread t1(thread1);
std::thread t2(thread2);
t1.join();
t2.join();
std::cout << "r1: " << r1 << ", r2: " << r2 << std::endl;
return 0;
}
输出行为
memory_order_relaxed
不保证操作顺序,可能输出r1 = 0, r2 = 0
,即两个线程可能完全看不到对方的操作。
适用场景
- 无依赖的原子计数器(如多线程中的计数统计),不需要线程间通信。
2. memory_order_acquire
和 memory_order_release
定义
memory_order_acquire
:防止之前的操作被重排序到当前操作之后。memory_order_release
:防止之后的操作被重排序到当前操作之前。
特点
-
配对使用
- 通常需要一个线程使用
memory_order_release
存储,另一个线程使用memory_order_acquire
加载。 - 能实现轻量级的线程同步。
- 通常需要一个线程使用
-
不保证全局顺序
- 仅适用于特定线程间的同步。
示例代码
#include <atomic>
#include <iostream>
#include <thread>
std::atomic<int> data{0};
std::atomic<bool> flag{false};
void thread1() {
data.store(42, std::memory_order_relaxed); // 存储数据
flag.store(true, std::memory_order_release); // 发布数据
}
void thread2() {
while (!flag.load(std::memory_order_acquire)); // 等待标志
std::cout << "Data: " << data.load(std::memory_order_relaxed) << std::endl; // 加载数据
}
int main() {
std::thread t1(thread1);
std::thread t2(thread2);
t1.join();
t2.join();
return 0;
}
输出行为
输出始终是:
Data: 42
线程 1 发布数据后,线程 2 确保读取到的数据是线程 1 存储的。
适用场景
- 用于生产者-消费者模式的轻量级同步。
3. memory_order_consume
定义
- 比
memory_order_acquire
更弱,仅禁止与数据依赖相关的重排序。
特点
- 几乎未被广泛实现,很多编译器直接将其等同于
memory_order_acquire
。 - 一般不建议使用。
4. memory_order_acq_rel
定义
- 结合了
memory_order_acquire
和memory_order_release
的特点。 - 同时保证“当前操作之前的代码不会被重排序到当前操作之后”和“当前操作之后的代码不会被重排序到当前操作之前”。
适用场景
- 在同一个操作中即需要加载又需要存储时(如比较交换
compare_exchange
)。
三、性能对比
内存序类型 | 内存屏障强度 | 性能 | 使用场景 |
---|---|---|---|
memory_order_relaxed | 无内存屏障,允许重排序 | 性能最好 | 无线程间同步依赖的场景 |
memory_order_acquire | 防止之前的操作被重排序到之后 | 较高性能 | 线程间轻量级同步 |
memory_order_release | 防止之后的操作被重排序到之前 | 较高性能 | 线程间轻量级同步 |
memory_order_acq_rel | 同时限制之前和之后的重排序 | 较低性能 | 加载和存储同时需要同步的场景 |
memory_order_seq_cst | 最严格,完全禁止重排序 | 性能最低 | 全局一致性顺序的多线程同步场景 |
总结
memory_order_seq_cst
是最安全但性能最低的内存序模型,适用于需要全局一致性的多线程程序。- 其他内存序(如
memory_order_relaxed
)允许更高性能,但需要程序员清楚理解线程间的同步关系,合理使用它们。 - 在实际开发中,尽量根据需求选择合适的内存序模型,不要一味追求性能而牺牲代码的正确性。