首页 > 代码库 > Memory Barriers

Memory Barriers

        这回该进入主题了。
        上一文最后提到了 Memory Barriers ,即内存屏障,因为对一个 CPU 而言,a = 1; b = 1. 由于在中间加了内存屏障,在 X86 架构下,就是 mfence 指令,此时在上一文中运行时,情况就变成这样了,当 CPU0 发
出 "read invalidate" 消息后,就会开始执行 mfence 指令,该指令把 Store Buffer 中的项都标记一下,然后开始执行 b = 1,此时虽然 cache hint (cache 命中),但是由于 Store Buffer 中存在被标记的项,所以
b 的新值不会直接应用到 cache 中,它也会被记录到 Store Buffer 中,并会等待被 Store Buffer 中被标记的项被应用,CPU0 也会继续执行其它指令。
        这样 CPU1 将会看到正确的 a 的值。看似完美的结局,其实还差那么一点点。
        依然是上一文的小例子。 a 和 b 初始化都为 0。 a 在 CPU1 的 cache 中, b 在 CPU0 的 cache 中。
        CPU 0 执行下面的代码:
        a = 1;
mfence;
        b = 1;
        CPU1 执行下面的代码:
        while (b != 1);
        assert (a == 1);
        断言一定能成功吗? 让我们假设下面的情况发生:
        1. CPU0 执行 a = 1, 但是发现 a 不在 cache 中,它就发出 "read invalidate" 消息,并且把 a 的值存在 store buffer 中;
        2。CPU1 执行 b != 1 的比较,读取 b 的值,结果不在 cache 中,因为它不想改写,所以只发一个 "read" 消息出去;
        3. CPU0 执行 mfence, 将 Store Buffer 中的项全部标记,然后执行 b = 1, 结果发现 Store Buffer 中有标记的项,所以就把 b 的值也记录在 Store Buffer,这些项是未标记的;
        4. CPU1 收到 "read invalidate" 消息,它把这个消息存入 Invalidate Queue 中,并作出回应;
        5. CPU0 收到回应,把 a 的值写入 Cache line, 把 b 的值也写入 cache line; 
        6. CPU0 收到 "read" 消息,并用出回应;
        7. CPU1 收到回应,退出循环,此时读取 a 的值,由于 Invalidate 消息还在队列中,此时它会认为 Cache line 中 a 的值有效,但其实为旧值,所以断言错误。
        很明显,兵败在了 Invalidate Queue 上,那可不可以向 Store Buffer 一样,弄一个屏障在读取 Cache line 时作一下检查呢?
        如果你能想到这里,说明赶上了设计 CPU 的那些家伙,对,这里也可以用一个 mfence.
        代码就成了这个样子
        while (b != 1);
        mfence;
        assert(a == 1);
        这样当 while (b != 1) 退出循环之后,遇到了 mfence, 它就必须停下来把 Message Queue 中的所以消息应用到 Cache line 中,此时就会发现 a 的 cache line 失效,当再进行读取 a 时,就会发消息给
CPU0 ,进而得到正确的结果。
        其实 X86 提供细粒度的指令 lfence (读屏障), sfence (写屏障), mfence(读写屏障)。
        lfence: 该指令会让 CPU 停下来,把 Message Queue 中存在的消息全部应用之后,才会执行后面的指令。这样读取就变得安全;
        sfence: 该指令把标记 Store Buffer 中所以已存在的写入记录项,当下次再有写入操作时,即使命中也不会直接应用到 cache 中,而是记录到 Store Buffer 中;
        mfence: 兼备以上两条指令的作用。
        还有一些其它指令具备这样的作用,一般如带 lock 前缀的指令等。所以一般情况下内存屏障需要成对使用。
        终于快要完了。

Memory Barriers