内存模型二:不同内存模型的差异

这次总结一下c++的两个内存模型。本来内存模型的概念应该是编程语言无关的。但是在众多高级编程语言中貌似只有c++支持使用不同的内存模型(Java只能使用顺序一致型内存模型)。

内存模型主要是和原子操作相关。在c++11中,一个原子对象,其load和store方法都有一个参数,用于指定操作所使用的内存序列(memory order),不同的内存序列对应不同的内存模型。

一般使用较多的主要有两个内存模型,分别是:

  1. SC模型(sequentially consistent),顺序一致性内存模型。
  2. AR模型(acquire/release),获取释放内存模型。

 

顺序一致性内存模型

SC模型,也是原子操作默认使用的内存模型。顺序一致性,说的是所有的处理器以相同的顺序看到所有的修改。可以把SC内存模型想象成:CPU和内存之间存在一个多路开关,连接到这个开关的CPU能够执行内存读写操作,因此能每一时刻有且只有一个CPU能操作内存。这意味着所有的内存操作都是串行执行的!SC模型保证了一个很强的内存顺序,如果一个原子操作A是顺序一致性的,那么它能保证,如果一个CPU观察到了A的发生,那么对于A之前的所有内存操作,都能被这个CPU观测到!上面一大段说白了其实就是:SC模型保证了一个内存操作对其它所有线程都同步(可见)

 

获取释放内存模型

程序执行的序列性越强,意味着效率越低。有些时候,或许不必使一个内存操作对于所有线程均同步(比如只想在某些特定的线程同步即可),因此可以允许更高程度的乱序来提高指令执行的效率。这便是其它内存模型存在的意义。具体来说,AR模型做的是,保证了执行store和load的两个线程之间同步,对于其它不相关的线程不作执行次序的要求,因此允许了这些线程为了执行效率而乱序执行指令。

 

作为对上述理论的补充,下面将给出几个实例。

例子一:一写多读

这个例子较为普遍,其中一个线程负责写某个数据,其它的线程只负责读数据。首先是最简单的,使用SC内存模型(atomic的load和store默认就是std::memory_order_seq_cst),代码如下:

atmoic<int> ready(0);

//thread 0(产生任务)
//======================
task_add();
ready.store(1);


//other threads(消费任务)
//======================
while (ready.load() != 1);
task_do();

 

使用AR模型优化一写多读

上面的代码是可以工作的,但是若想更精准的控制线程之间的同步,就要使用AR模型了,具体代码如下:

atmoic<int> ready(0);

//thread 0(产生任务)
//======================
task_add();
ready.store(1, std::memory_order_release);


//other threads(消费任务)
//======================
while (ready.load(std::memory_order_acquire) != 1);
task_do();

如此一来,同步仅发生在必要的线程之内了!

 

例子二:多写多读

多写多读是例子一的扩展,一些线程负责写数据,一些线程负责读数据。同样的,先是使用SC模型:

atmoic<int> ready(0);

//some threads(产生任务)
//======================
task_add();
ready.store(1);


//other threads(消费任务)
//======================
while (ready.load() != 1);
task_do();

值得一提的是,代码和一写多读的一样!如果是使用SC模型的话,并不需要区分究竟是一个线程写数据,还是多个线程写数据。因为SC模型的效果就是,所有读写看上去都是串行的!

 

使用AR模型对多写多读进行错误的优化

多写多读对AR模型稍有不同,稍不注意就会跌入大坑。先看一个错误的例子:

atmoic<int> ready(0);

//some threads(产生任务)
//======================
task_add();
ready.store(1, std::memory_order_release);


//other threads(消费任务)
//======================
while (ready.load(std::memory_order_acquire) != 1);
task_do();

直接把一写多读的代码搬过来可行吗?读操作与写操作之间是有同步的,看上去似乎是没有问题。但是由于是多个线程都有机会写数据,那么多个线程的写操作之间的同步呢?

 

使用AR模型对多写多读进行正确的优化

atmoic<int> ready(0);

//some threads (产生任务)
//======================
task_add();
ready.exchange(1, std::memory_order_acq_rel);


//other threads(消费任务)
//======================
while (ready.load(std::memory_order_acquire) != 1);
task_do();

这样一来就保证了不同线程的写操作之间也会进行必要的同步!

 

总结 

当然还有其它的内存模型,比如对次序不作任何保证的Relax。不同的内存模型对多线程乱序做了不同程度的保证。在保证程序执行正确性的前提下,根据不同的应用场景选择最合适的内存模型,允许尽可能多的乱序,是提高执行效率的一种方法!

posted @ 2017-01-29 01:41  adinosaur  阅读(959)  评论(0编辑  收藏  举报