为什么需要内存模型

编译器会优化代码,会在不影响正确结果的前提下,调整代码执行顺序,此外cpu也会乱序执行,提高效率。所以,你的代码跑起来未必是按照你写的顺序执行的。这在单线程环境下毫无问题,一旦进入多线程,这种乱序会带来不可预期的结果。

因此,我们需要一种方式来(向底层:编译器)控制,告诉,声明我这段代码必须要按顺序执行。内存模型应运而生。

此外,还有

Cache coherency

现代cpu拥有多级cache,其中cache是有访问权限的。有些cahce中的数据只能被一个核心看到,这在多线程访问数据时就会出现很大的问题。

例如:

正常理解,无论线程1,2的执行顺序如何,最后不可能出现打印出0,0的情况。但是在这种情况下,如果各自都从缓存中读数据,那就可能出现0,0的情况。这也进一步引出了volatile关键字volatile wiki link

Sequential Consistency

顺序一致性,是一致性最严格的内存模型,他有两条要求:

  1. 每个处理器的执行顺序和代码的执行顺序一致
  2. 处理器并行执行的结果要和处理器按照某种顺序执行的结果一致

Memory barrier

即内存栅栏。为什么需要内存栅栏?

memory_order解释
memory_order_acquire所有内存操作必须在我这个load之后
memory_order_release所有内存操作必须在我这个release之前

有两个因素会影响执行顺序:

  • 编译器优化,重排
  • 处理器乱序
    • ARM 和 Alpha 更为松散
    • 即使 Intel 处理器仍有乱序

在c++98里,我们没有一种机制能够告诉c++编译器,我们在某个代码块不希望乱序。我们不可能阻止所有乱序,但在多线程同步场合下,我们确实需要保证顺序。

acquire、release语义

解决通过单个原子量进行同步的场景。

acquire指的是,我在这里做了一个原子读操作,我要求在我后面的所有读操作不允许重排到我前面去。

relase说的是,我这儿做了个原子写操作,我要求在我前面的写操作不允许重排到我后面去。

这两个配合使用可以做到两个线程间的同步!假设线程A store an atmoic variable using memory_order_release,线程B load the same atomic variable using memory_order_acquire.

根据acquire和release的语义,线程A中所有发生在atomic store之前的内存写(包括非原子的和松弛原子【memory_order_relaxed】的),在线程B中将变得可见。也就是说,当线程B atmoic load之后且load到的值确实是线程A写入的那个值,则B能够保证看到everything thread A wrote to memory.

这个同步仅仅存在于load with acuqire, store with release, on the same atomic variable的任意两个线程之间。其他线程不保证。

Mutual exclusion locks, such as std::mutex or atomic spinlock, are an example of release-acquire synchronization: when the lock is released by thread A and acquired by thread B, everything that took place in the critical section (before the release) in the context of thread A has to be visible to thread B (after the acquire) which is executing the same critical section.

内存屏障:内核文档