C++的内存模型
这些文章对于内存模型的讲解非常好,建议阅读,本文整理一下相关的知识点。
Memory Barrier的类型
LoadLoad
有效的保护了在屏障之前执行的读取和屏障之后执行的读取。
举个例子,
git pull
命令,如果在执行中有其他的merge conflicts(合并分支的冲突),这时无法保证会提取数据库的最新版本。使用LOADLOAD_FENCE()
可以保证不会看到过时的数据,1
2
3
4
5if(IsPublished)
{
LOADLOAD_FENCE();
return value;
}StoreStore
有效的保护了在屏障之前的存储和屏障之后的存储
同样地,考虑
git push
的过程,同时该过程是延迟、异步的行为。所以无法保证在执行push之后,无法保证它之前存储的数据在整个数据库中是可见的。1
2
3value = 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 | namespace std |
memory_order_seq_cst
顺序一致性,默认的选项,不允许reorder
memory_order_release/acquire/acq_rel
release和acquire的重排都可能造成存取过程的不同步,acq_rel是保证了线程不同时进入临界区。
memory_order_relaxed
宽松操作,没有同步或顺序的制约,仅仅对
load()
和store()
操作保证原子性。
编写测试用例时,用到全局变量,让两个线程对其读/写就可以看到race condition(x86架构下只要是原子变量不会出现竞态,硬件保证了获取/释放语义)。