C++ Memory Order

C++11 Memory Order

为什么需要Memory Order

多线程下可能影响程序执行结果的行为:

  • 原子操作(简单语句,C++也不保证是原子操作)
  • 指令执行顺序(编译器可能优化代码使代码顺序发生变化,CPU可能会调整指令执行顺序)
  • CPU可见性(再CPU cache的影响下,可能存在一个CPU执行了某个指令,不会立即被其他CPU所见)

例子1

int64_t i = 0;
// Thread-1
i = 100;

// Thread2
std::cout << i;

上面的代码输出可能是一个未定义的行为,由于不同CPU结构对指令的处理也不同,有的CPU再处理64为字节时需要两条指令,那这样i就不是原子操作,Thread-1和Thread-2的行为就变成了未定义.

例子2

std::atomic<int> x{0};
std::atomic<int> y{0};

// Thread-1:
x.store(100, std::memory_order_relaxed);
y.store(200, std::memory_order_relaxed);

// Thread-2:
while (y.load(std::memory_order_relaxed) != 200)
    ;
std::cout << x.load(std::memory_order_relaxed);

上面的例子,线程1对x和y分别写入100,200,并且x, y的操作属于原子操作,线程2等待y变成200之后,输出x的值,在这里x的值可能是0,由于再多线程下并没有保证指令指令顺序的一致性,所以上面的顺序是两个字:任意,那y.strore就可能执行再x.stroe的前面,导致x输出0.

例子3

int x = 0;

// Thread-1:
x = 100;    // A

// Thread-2:
std::cout << x; // B

现在假设A在B之前执行,x的可能输出是多少?

x可能输出0,100,由于CPU可见性的问题,可能Thread-1中的A操作执行完,x的值还在高速缓存上,并没有更新到内存,之后Thread-2中的B去读取x的值就读取到了旧值.

以上三个例子分别对应的三种可能改变程序执行结果的行为(多线程下),C++11 memory order就是解决以上的问题,让C++用户写多线程程序时不需要考虑机器环境(这个时C++11 memory model功劳), 在C++11中可以提供了mutex,std::atomic...

C++11 std::atomic

c11 的顺序格式如下:

  • Relaxed ordering
  • Release_Acquire ordering
  • Release_Consume ordering
  • Sequentially-consistent ordering

分别对应了std::atomic中的顺序如下:

  • std::memory_order_relaxed
  • std::memory_order_acquire
  • std::memory_order_release
  • std::memory_order_consume
  • std::memory_acq_rel
  • std::memory_seq_cst

Relaxed ordering

std::atomic<int> x = 0;
std::atomic<int> y = 0;

// Thread-1:
r1 = y.load(memory_order_relaxed);  // A
x.store(r1, memory_order_relaxed);  // B

// Thread-2:
r2 = x.load(memory_order_relaxed);  // C
y.stroe(99, memory_order_relaxed);  // D

r1 == r2 == 99可能出现吗?

可能的, D->A->B->C的执行顺序就可能出现r1 == r2 == 99

解释: Relaxed ordering仅仅只保证atomic自带的方法是原子操作,除此之外,不提供任何跨线程的同步,由于不提供任何跨线程的同步,所以上面例子中的执行顺序就是两个字:任意,但是Relaxed ordering好处也非常明显,就是性能高.

Release-Acquire ordering

在这种模型下,load()采用memory_order_acquire, store()采用memory_order_release,提供了两个线程之间的主要功能.

1.限制指令重排

  • 在store()之前的所有读写操作,不允许被移动到这个store()的后面.
  • 在load()之后的所有读写操作,不允许被移动到这个load()前面.

2.线程之前可见

线程之间由于Cpu cache,导致线程之间不可见,Release-Acquire提供了线程之间的可见.

std::atomic<bool> ready{false};
int data = 0;
void producer() {
    data = 10;                                      // A
    ready.store(true, std::memroy_order_release);   // B
}

void consumer() {
    while (!ready.load(std::memory_order_acquire))  // C
        ;
    assert(data == 100);                            // D
}

int main() {
    std::thread t1(producer);
    std::thread t2(consumer);
}

上面例子中assert永远不会触发,根据上面的指令限制规则,A不可能移动到B的后面,D不可能移动到C的前面.

std::atomic<bool> f1 = false;
std::atomic<bool> f2 = false;

// thread1
f1.store(true, memroy_order_release);   // A
if (!f2.load(memory_order_acquire)) {   // B
    // critical section
}

//thread2
f2.store(true, memroy_order_release);   // C
if (!f1.load(memory_order_acquire)) {   // D
    // critical section
}

上面例子中两个if条件可能同时满足,并且同时进入临界区,根据上面的指令规则,B可以移动到A的前面,D可以移动到C的前面,这都不违法规则.

Release-Consume ordering

Release-Consume相比Release-Acquire,在功能上弱了一点,它仅仅将依赖于x(假设)的值进行同步.

Sequential consisten ordering

Release-Acquire就同步一个x,顺序一致会对所有的变量的所有原子操作都同步,这样,所有的原子操作就跟由一个线程执行似的.

posted @ 2021-07-18 22:48  gd_沐辰  阅读(341)  评论(0编辑  收藏  举报