C++的内存序

C++
Author

0warning0error

Published

April 7, 2025

什么是内存序

在现代CPU中,指令的执行顺序可能会被CPU重排序,乱序执行从而提高执行的性能。这导致在某些情况在任意时刻得到的结果并不会按照预期顺序执行的一样。内存序就是控制CPU重排序的程度。

C++11 为std::atomic提供了 6 种 memory ordering:

  • memory_order_relaxed
  • memory_order_consume(不用了解,用的少,而且C++26也要放弃它)
  • memory_order_acquire
  • memory_order_release
  • memory_order_acq_rel
  • memory_order_seq_cst

默认情况下,std::atomic使用的是 memory_order_seq_cst。 但在某些场景下,合理使用其它的内存序,可以让编译器优化生成的代码,从而提高性能。

relaxed 序

在这种模型下,Relaxed 内存序仅仅保证load()和store()是原子操作,除此之外,不提供任何跨线程的同步。

例如对于初始值为0的 x 和 y,

// 线程 1:
r1 = y.load(std::memory_order_relaxed); // A
x.store(r1, std::memory_order_relaxed); // B
// 线程 2:
r2 = x.load(std::memory_order_relaxed); // C 
y.store(42, std::memory_order_relaxed); // D

有可能会出现r1 == r2 == 42, 即使线程1中A先于B执行 且线程 2 中 C 先于 D执行,却无法避免CPU乱序执行,D 会出现于 A 之前,B 会出现于 C 之前,最后形成D A B C 的执行顺序。

Relaxed 内存序的典型的应用是计数器自增,std::shared_ptr 的引用计数器,因为这只要求原子性,但不要求定序或同步(注意 std::shared_ptr 计数器的自减要求与析构函数间进行获得-释放同步)。

release-acquire 序

若线程 A 中的一个原子以memory_order_release的内存序store ,而线程 B 中从同一变量以memory_order_acquire原子load,且线程 B 中的加载读到了线程 A 中的存储所写入的值,则线程 A 中的存储同步于线程 B 中的加载。从线程 A 的视角先发生于原子存储的所有内存写入(包括非原子及宽松原子的),在线程 B 中一旦原子加载完成,则保证线程 B 能观察到线程 A 写入内存的所有内容。(这其实是同步点的概念)仅当 B 实际上返回了 A 所存储的值或其释放序列中后面的值时,才有此保证。 也就是说,如果B读到了A原子写入的值,那么A原子写入的前面所有的操作都能被B看见

#include <atomic>
#include <cassert>
#include <string>
#include <thread>
 
std::atomic<std::string*> ptr;
int data;
 
void producer()
{
    std::string* p = new std::string("Hello");
    data = 42;
    ptr.store(p, std::memory_order_release);
}
 
void consumer()
{
    std::string* p2;
    while (!(p2 = ptr.load(std::memory_order_acquire)))
        ;
    assert(*p2 == "Hello"); // 一定能读到p2指向的字符串对象
    assert(data == 42); // 一定能读到data值为42
}
 
int main()
{
    std::thread t1(producer);
    std::thread t2(consumer);
    t1.join(); t2.join();
}

seq_cst 序

seq_cst 序是C++中最严格的内存顺序模型,它保证了所有线程看到的内存操作顺序与代码编写顺序一致,且所有操作形成一个全局唯一的执行顺序。以下是通俗解释:

想象所有使用memory_order_seq_cst的操作被排成一个全局队列,所有线程看到的操作顺序完全相同。例如: - 线程A先写变量x,线程B后写变量y,则所有线程都会观察到x的修改发生在y之前。 - 即使不同线程的操作涉及不同变量,它们的顺序在全局队列中也是固定的。

例如:

// 线程1
x.store(1, std::memory_order_seq_cst); // 操作A
y.store(1, std::memory_order_seq_cst); // 操作B

// 线程2
int a = y.load(std::memory_order_seq_cst); // 操作C
int b = x.load(std::memory_order_seq_cst); // 操作D

所有线程会看到操作顺序为A→B→C→D,或A→C→B→D等,但不会出现B在A之前的情况。

场景

  • 避免矛盾观察:若两个线程分别修改xy,其他线程无法看到x先改后y改,同时另一线程看到y先改后x改的情况。
// 线程1修改x和y(顺序一致)
x.store(true, std::memory_order_seq_cst);
y.store(true, std::memory_order_seq_cst);

// 线程2和3分别读取
// 所有线程看到的顺序要么x→y,要么y→x,但不会矛盾。

然而,一旦混用非Seq-Cst操作,全局一致性可能被破坏。例如:

// 线程1(Seq-Cst写x,Release写y)
x.store(1, std::memory_order_seq_cst); // A
y.store(1, std::memory_order_release); // B

// 线程2(Seq-Cst读y,Relaxed读y)
r1 = y.fetch_add(1, std::memory_order_seq_cst); // C
r2 = y.load(std::memory_order_relaxed);         // D

// 线程3(Seq-Cst写y,Seq-Cst读x)
y.store(3, std::memory_order_seq_cst); // E
r3 = x.load(std::memory_order_seq_cst); // F

可能出现r1=1, r2=3, r3=0,即线程3的读操作F看到x的旧值(0),因为全局顺序可能是C→E→F→A。

Seq-Cst操作需要插入硬件级内存屏障(如x86的mfence),强制所有核心同步内存状态,导致较高开销。所以需要所有线程对操作顺序达成一致时(如分布式锁、严格顺序计数器)才会考虑使用。