C++ 内存序模型(Memory Model)
1. C++ 内存模型概览
C++11 引入了内存模型(memory model),主要解决多线程共享内存访问的可见性与顺序性问题。它定义了:
- 程序顺序(program order):单线程内的顺序是固定的。
- 多线程访问的顺序(inter-thread ordering):不同线程对共享变量的访问可能乱序。
- 原子操作(atomic operations):保证对某个共享对象的操作是不可中断的。
- 内存序(memory order):定义原子操作的可见性、同步约束。
在 C++ 中,关键组件是:
std::atomic<T>:原子变量模板std::memory_order:原子操作的内存序std::mutex、std::condition_variable:高级同步原语(内部依赖内存序)
2. 原子操作(Atomic Operations)
std::atomic<int> counter{0};
counter.fetch_add(1, std::memory_order_seq_cst); // 原子自增
特点:
- 不可分割(atomic)
- 防止数据竞争(data race)
- 配合内存序可以控制可见性和顺序
注意:非原子变量在多线程写入时,如果没有同步机制,将会产生数据竞争,这是未定义行为。
3. C++ 内存序类型
C++ 定义了 6 种内存序,按严格性从强到弱排列:
| 内存序 | 含义 | 顺序约束 | 典型用途 |
|---|---|---|---|
memory_order_seq_cst | 顺序一致 | 最严格,所有线程对同一原子操作的观察一致 | 默认选择,简单、可靠 |
memory_order_acquire | 获取 | 对当前线程之后的读写不会被重排到 acquire 之前 | 读锁、标志位同步 |
memory_order_release | 释放 | 对当前线程之前的读写不会被重排到 release 之后 | 写锁、发布数据 |
memory_order_acq_rel | 获取-释放 | combine acquire + release | 读写锁 |
memory_order_relaxed | 放宽 | 不保证跨线程顺序,仅保证原子性 | 高性能计数器、统计 |
memory_order_consume | 数据依赖 | 减少 acquire 的开销,但依赖支持有限 | 特殊优化 |
4. 内存序示意
考虑两个线程共享变量 x 和 y:
std::atomic<int> x{0}, y{0};
int r1, r2;Thread 1:
x.store(1, std::memory_order_release);
r1 = y.load(std::memory_order_acquire);Thread 2:
y.store(1, std::memory_order_release);
r2 = x.load(std::memory_order_acquire);
解释:
store(..., release)确保发布之前的写操作对其他线程可见load(..., acquire)确保获取之后的读写不会提前- 如果只用
relaxed,可能出现r1==0 && r2==0,即重排序导致线程看不到对方的写
5. 内存序规则总结
- Release-Acquire:确保线程间数据同步
- Relaxed:只保证原子性,不保证顺序
- Seq_Cst:全局单一顺序,保证最强一致性
- 依赖链:Acquire-Release 可通过数据依赖传播可见性
6. 内存屏障(Memory Fences)
C++ 提供了 atomic_thread_fence,用于手动插入内存屏障:
std::atomic_thread_fence(std::memory_order_acquire);
std::atomic_thread_fence(std::memory_order_release);
std::atomic_thread_fence(std::memory_order_seq_cst);
作用:
- 阻止编译器和 CPU 对原子操作前后的普通内存访问进行乱序
- 常用于 lock-free 数据结构(队列、环形缓冲区)同步
7. 内存序与 CPU 架构
不同 CPU 架构对内存序的支持不同:
| CPU | 默认顺序 | 特殊说明 |
|---|---|---|
| x86 | Total Store Order (TSO) | store-store 不乱序,load-store 可乱序 |
| ARM | Weakly Ordered | load-store 可能乱序,需要 fence |
| POWER | Very Weak | 需要 fence 实现 acquire/release |
C++ 的内存序会在编译器层面生成对应的 CPU 指令(mfence、dmb、sync 等)。
8. 常用模式示例
8.1 Release-Acquire 发布标志位
std::atomic<bool> ready{false};
int data = 0;void producer() {data = 42;ready.store(true, std::memory_order_release);
}void consumer() {while(!ready.load(std::memory_order_acquire));// 此时保证 data 可见std::cout << data << std::endl; // 42
}
8.2 Relaxed 计数器
std::atomic<int> counter{0};
counter.fetch_add(1, std::memory_order_relaxed); // 高性能计数
8.3 Seq_Cst 全局顺序保证
std::atomic<int> x{0};
x.store(1); // 默认 seq_cst
int r = x.load(); // 保证所有线程看到一致顺序
9. 小结与实践建议
- 默认选择
seq_cst:简单可靠,避免错综复杂的同步 bug - 性能优化时使用
relaxed:只用于不依赖顺序的统计计数、采样 - Release-Acquire 是锁自由数据结构的核心
- 避免直接用非原子共享变量多线程写入
- 结合 mutex/condition_variable 更简单:C++ 内存序复杂,很多场景 mutex 性能和正确性都更优
10.应用示例
示例 1:锁自由单生产者单消费者队列(SPSC Queue)
特点:生产者写入,消费者读取,要求高性能,使用 Release-Acquire 保证同步。
#include <atomic>
#include <vector>
#include <iostream>template <typename T>
class SPSCQueue {std::vector<T> buffer;const size_t size;std::atomic<size_t> head{0};std::atomic<size_t> tail{0};public:SPSCQueue(size_t sz) : size(sz), buffer(sz) {}bool push(const T& value) {size_t h = head.load(std::memory_order_relaxed);size_t next = (h + 1) % size;if (next == tail.load(std::memory_order_acquire)) // 空间检查return false;buffer[h] = value;head.store(next, std::memory_order_release); // 发布数据return true;}bool pop(T& value) {size_t t = tail.load(std::memory_order_relaxed);if (t == head.load(std::memory_order_acquire)) // 队列空return false;value = buffer[t];tail.store((t + 1) % size, std::memory_order_release); // 更新 tailreturn true;}
};
说明:
head发布写入,消费者获取时acquire保证看到 buffer 中的数据- 无需锁,性能高,适合实时 SLAM 数据管线
示例 2:跨线程标志位同步
场景:生产者准备数据,消费者等待数据可用。使用 Release-Acquire 内存序保证数据可见。
#include <atomic>
#include <thread>
#include <iostream>std::atomic<bool> ready{false};
int shared_data = 0;void producer() {shared_data = 100; // 写入数据ready.store(true, std::memory_order_release); // 发布标志
}void consumer() {while(!ready.load(std::memory_order_acquire)) {} // 获取标志std::cout << "Data = " << shared_data << std::endl; // 确保读到正确数据
}int main() {std::thread t1(producer);std::thread t2(consumer);t1.join(); t2.join();
}
说明:
- 如果使用
relaxed,消费者可能看到ready=true但shared_data尚未更新 release和acquire形成同步,确保跨线程数据顺序正确
示例 3:高性能统计计数器(Relaxed 内存序)
场景:多线程累加统计量,不依赖精确顺序,仅保证原子性。
#include <atomic>
#include <thread>
#include <vector>
#include <iostream>std::atomic<int> counter{0};void worker(int n) {for (int i = 0; i < n; i++) {counter.fetch_add(1, std::memory_order_relaxed); // 高性能累加}
}int main() {const int N = 1000000;std::thread t1(worker, N);std::thread t2(worker, N);t1.join(); t2.join();std::cout << "Total count = " << counter.load() << std::endl;
}
说明:
- 使用
relaxed可以避免不必要的 fence 指令,提高性能 - 只要最终累加结果一致即可,顺序无关紧要
总结
| 示例 | 类型 | 内存序 | 说明 |
|---|---|---|---|
| 1 | SPSC Queue | release/acquire | 锁自由队列,保证生产者写入数据对消费者可见 |
| 2 | 跨线程标志位 | release/acquire | 发布数据+等待标志同步 |
| 3 | 高性能计数器 | relaxed | 高性能累加,不依赖顺序 |
