0%

C++内存模型

C++的内存模型

弱vs强内存模型

编译时内存排序

运行时内存排序

理解C++的Memory Order

这些文章对于内存模型的讲解非常好,建议阅读,本文整理一下相关的知识点。

Memory Barrier的类型

  • LoadLoad

    有效的保护了在屏障之前执行的读取和屏障之后执行的读取。

    举个例子,git pull命令,如果在执行中有其他的merge conflicts(合并分支的冲突),这时无法保证会提取数据库的最新版本。使用LOADLOAD_FENCE()可以保证不会看到过时的数据,

    1
    2
    3
    4
    5
    if(IsPublished)
    {
    LOADLOAD_FENCE();
    return value;
    }
  • StoreStore

    有效的保护了在屏障之前的存储和屏障之后的存储

    同样地,考虑git push的过程,同时该过程是延迟、异步的行为。所以无法保证在执行push之后,无法保证它之前存储的数据在整个数据库中是可见的。

    1
    2
    3
    value = x;
    STORESOTRE_FENCE();
    IsPublished = 1; //标记一下存储完成
  • LoadStore

    从内存中拷贝到cpu,再从cpu拷贝到内存,这种指令会重排。

    在实际的cpu中,如果load时高速缓存未命中,然后在存储上命中高速缓存,某些处理器就会发生重排,让其先存储再读取。

  • StoreLoad

    确保屏障执行之前的存储对所有处理器都是可见的,并且屏障执行之后的读取都是接收的可见的最新值,实现了顺序一致性(唯一一种可以防止race condition的内存屏障)。

硬件层面内存模型

不同的处理器在内存重新排序的过程中具有不同的习惯,只能在多核/多处理器中观察到。如上方博客中所介绍,x86/64架构的计算机普遍是strong memory model。

那么硬件层面的强/弱内存模型区别是什么呢?A strong hardware memory model is one in which every machine instruction comes implicitly with acquire and release semantics. As a result, when one CPU core performs a sequence of writes, every other CPU core sees those values change in the same order that they were written.文章中是这么描述的,意思是每条机器指令都隐式地带有获取/释放语义。因此,当一个核执行一系列写操作时,每一个cpu的核都能看到写入的值和写入的顺序。也就是说不会存在 loadload、loadstore、storestore 的重排,可以进行的是 storeload 的重排。

Note: acquire语义是指load之后的读写操作无法被重排到load之前;release语义是指store之前的操作无法被重排到store之后。

https://preshing.com/20120515/memory-reordering-caught-in-the-act/

上方链接是一个示例程序显示重排发生的原因。主要思路是在信号量sem_wait()之后对共享内存进行写入操作,在sem_post()之前对共享内存进行读取操作,这样确保acquire/release的语义,这是一种不会发生重排的做法。如果用两个信号量给两个线程发送(sem_post),操作一番,等待两个线程(sem_wait),再用一个for循环统计迭代多少次以后会发生重排(如果数值不符合预期就是发生了重排)。会发现60万次迭代中有90多次发生了重排。

如果想消除这些重新排序,有这么几种做法:

(1)提高线程关联性,让两个工作线程都在同一个cpu上运行,但是可移植性差;

(2)引入cpu屏障,StoreLoad Barrier,在gcc中可以用`asm volatile(“mfence” ::: “memory”),防止内存重新排序

软件层面内存模型

如果在语言层面实现对多线程共享变量的控制,忽略编译器和cpu架构的不同对多线程编程的影响,这样就可以跨平台了。

在C++11之后提供了六种不同的memory_order选项

1
2
3
4
5
6
7
8
namespace std
{
typedef enum memory_order
{
memory_order_relaxed, memory_order_consume, memory_order_acquire,
memory_order_release, memory_order_acq_rel, memory_order_seq_cst
} memory_order;
}
  • memory_order_seq_cst

    顺序一致性,默认的选项,不允许reorder

  • memory_order_release/acquire/acq_rel

    release和acquire的重排都可能造成存取过程的不同步,acq_rel是保证了线程不同时进入临界区。

  • memory_order_relaxed

    宽松操作,没有同步或顺序的制约,仅仅对load()store()操作保证原子性。

编写测试用例时,用到全局变量,让两个线程对其读/写就可以看到race condition(x86架构下只要是原子变量不会出现竞态,硬件保证了获取/释放语义)。