为什么需要内存模型
编译器会优化代码,会在不影响正确结果的前提下,调整代码执行顺序,此外cpu也会乱序执行,提高效率。所以,你的代码跑起来未必是按照你写的顺序执行的。这在单线程环境下毫无问题,一旦进入多线程,这种乱序会带来不可预期的结果。
因此,我们需要一种方式来(向底层:编译器)控制,告诉,声明我这段代码必须要按顺序执行。内存模型应运而生。
此外,还有
Cache coherency
现代cpu拥有多级cache,其中cache是有访问权限的。有些cahce中的数据只能被一个核心看到,这在多线程访问数据时就会出现很大的问题。
例如:
正常理解,无论线程1,2的执行顺序如何,最后不可能出现打印出0,0的情况。但是在这种情况下,如果各自都从缓存中读数据,那就可能出现0,0的情况。这也进一步引出了volatile关键字volatile wiki link。
Sequential Consistency
顺序一致性,是一致性最严格的内存模型,他有两条要求:
- 每个处理器的执行顺序和代码的执行顺序一致
- 处理器并行执行的结果要和处理器按照某种顺序执行的结果一致
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.
内存屏障:内核文档