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(读写屏障)。spa
lfence: 该指令把当前invalidate queue 里的全部项标记。当 load 动做发生时,假设队列中有标记项。那么 CPU 必须把 Message Queue 中存在的消息全部应用以后,才会运行后面的指令。设计
这样读取就变得安全;
sfence: 该指令把标记 Store Buffer 中因此已存在的写入记录项。当下次再有写入操做时,即便命中也不会直接应用到 cache 中。而是记录到 Store Buffer 中。
mfence: 兼备以上两条指令的做用。
另外一些其余指令具有这种做用,通常如带 lock 前缀的指令等。队列
因此普通状况下内存屏障需要成对使用。
最终快要完了。 内存