1 说明

多核系统多每个CPU直接对缓存进行读写, 可能会导致某个CPU写入的数据另一个CPU无法看到. 而且CPU会根据需要对指令(这里特指内存指令, load和store)乱序执行, 在单核CPU中这种乱序会保证结果与顺序执行一样, 但是多线程环境下可能出错.

这时候需要memory barrier来告诉CPU哪些可以乱序, 哪些不可以.

2 Memory Barrier种类

原始网址

http://preshing.com/20120710/memory-barriers-are-like-source-control-operations/

文中将CPU的结构: memory_ordering_20150914_172419.png

比喻为版本控制的流程. memory_ordering_20150914_172450.png

Core 1对内存的读写只反映在L1 Cache中, 什么时候更新到L2 Cache或者Ram这个公共的存储中, 或者什么时候从公共存储中更新回自己的L1 Cache并不确定. 更新顺序也不能保证, 比如先修改了a, 再修改b和c, 可能b和c更新到了共享存储中而a没有.

LoadLoad

在此barrier之前的load不要调整到之后去, 同样之后的load也不要调整到之前.

if (IsPublished)                   // Load and check shared flag
{
    LOADLOAD_FENCE();              // Prevent reordering of loads
    return Value;                  // Load published value
}

可以把LOADLOAD_FENCE类比为git pull. 尽管pull来的Value并不能保证一定是整个系统最新的, 但是可以保证Value的值一定比LOADLOAD_FENCE之前的LOAD的值(也就是这里的IsPublished)新. 最起码是一样新.

如果没有LOADLOAD_FENCE, CPU在执行时可能先把Value保存到寄存器, 再把IsPublish load进来, 再判断IsPublish的值. 如果在Value和IsPublished两个load之间被另一个线程将IsPublished改了, 那么return Value返回的值会是旧的那个. 而如果插入LoadLoad屏障, 线程在看到IsPublished被改变时, 另一个线程已然把Value改好(另一个线程也要保证顺序性, 见StoreStore).

StoreStore

在此Barrier之前和之后的Store不会被互相调整到另一边. 不仅如此, 屏障之后store的数据

Value = x;                         // Publish some data
STORESTORE_FENCE();
IsPublished = 1;                   // Set shared flag to indicate availability of data

用版本控制比喻的话就是git push. 这个FENCE还保证IsPublished肯定在Value 之后被更新到下一级存储. 注意这里并不是说FENCE之后IsPublished肯定马上被更新, 只是说如果IsPublished被更新了, 那么Value肯定也已经被更新了.

LoadStore

在此Barrier之前的Load必须比在此之后的Store先执行.

StoreLoad

在此之前的Store肯定比之后Load先执行.

类似版本控制的话, StoreLoad可以被看做是一个完全同步, 把所有改动更新到服务器, 然后pull下来最新的版本. 也就是说, StoreLoad保证了执行到这条命令时, 所有在此之前的Store指令相关的数据已经可以被别的处理器看见(visible), 而且所有在此之后的Load指令的相关数据本处理器也已经可以看见.

与#StoreStore紧跟一条#LoadLoad的不同之处在于, #StoreStore可以任意时间之后才push, LoadLoad可能并不pull下来最新的.

#StoreLoad是一个比较昂贵的操作.

3 约束

x86机器上其实对内存的reordering并不多. 所以很多情况下我们没有刻意用memory barrier也能正确执行代码.

4 Memory Ordering in C++

网址

http://preshing.com/20130930/double-checked-locking-is-fixed-in-cpp11/

介绍了跨平台的Singleton在C++11前无法被实现. 因为双检锁机制在没有内存屏障语句时遇到Memory Ordering可能会出现错误(x86的memory ordering比不多, 所以没问题). 到了C++11, 直接static singleton可以保证这个变量在多线程中只被初始化一次. 可以认为其内部实现就是用到了memory_order的内存屏障.

Leave a Reply

电子邮件地址不会被公开。 必填项已用*标注

You may use these HTML tags and attributes:

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>