volatile的内存屏障的坑 (2)

上面我说道:在实际运行中可以确定某些“上载”和“下载”操作(指令)不会互相影响,
这里有一个前提条件哈:该假设仅基于基于线程的依赖性检查进行(per-thread basis dependency checks)。
虽然在单个线程是可以被确定为指令独立性,但CPU无法考虑多个线程的情况,所以提供了【volatile关键字】

我们回到上面的示例,尽管我们已将字段标记为volatile,但感觉上没有起作用。为什么?

一般说道volatile我都一般都会举下面的例子(内存可见性)

using System; using System.Threading; public class C { bool completed; static void Main() { C c = new C(); var t = new Thread (() => { bool toggle = false; while (!c.completed) toggle = !toggle; }); t.Start(); Thread.Sleep (1000); c.completed = true; t.Join(); // Blocks indefinitely } }

如果您使用release模式运行上述代码,它也会无限死循环。
这次CPU没有罪,但罪魁祸首是JIT优化。

你如果把:

bool completed;

改成

volatile bool completed;

就不会死循环了。
让我们来看一下[没有加volatile]和[加了volatile]这2种情况的IL代码:

没有加volatile L0000: xor eax, eax L0002: mov rdx, [rcx+8] L0006: movzx edx, byte ptr [rdx+8] L000a: test edx, edx L000c: jne short L001a L000e: test eax, eax L0010: sete al L0013: movzx eax, al L0016: test edx, edx # <-- 注意看这里 L0018: je short L000e L001a: ret 加了volatile L0000: xor eax, eax L0002: mov rdx, [rcx+8] L0006: cmp byte ptr [rdx+8], 0 L000a: jne short L001e L000c: mov rdx, [rcx+8] L0010: test eax, eax L0012: sete al L0015: movzx eax, al L0018: cmp byte ptr [rdx+8], 0 <-- 注意看这里 L001c: je short L0010 L001e: ret

留意我打了注释的那行。上面的这些IL代码行 实际上是代码进行检查的地方:

while (!c.completed)

当不使用volatile时,JIT将完成的值缓存到寄存器(edx),然后仅使用edx寄存器的值来判断(while (!c.completed))。
但是,当我们使用volatile时,将强制JIT不进行缓存,
而是每次我们需要读取它直接访问内存的值 (cmp byte ptr [rdx+8], 0)

JIT缓存到寄存器 是因为 发现了 内存访问的速度慢了100倍以上,就像CPU一样,JIT出于良好的意图,缓存了变量。
因此它无法检测到别的线程中的修改。
volatile解决了这里的问题,迫使JIT不进行缓存。

说完可见性了我们在来说下volatile的另外一个特性:内存屏障

确保在执行下一个上传/下载指令之前,已完成从volatile变量的下载指令。

确保在执行对​​volatile变量的当前上传指令之前,完成了上一个上传/下载指令。

但是volatile并不禁止在完成上一条上传指令之前完成对volatile变量的下载指令。
CPU可以并行执行并可以继续执行任何先执行的操作。
正是由于volatile关键字无法阻止,所以这就是这里发生的情况:

mov dword ptr [rax+0xc], 1 # 把值 1 上传到内存地址 'y' mov edx, [rax+8]. # 从内存地址 'x' 下载值并放到edx(寄存器)

变成这个

mov edx, [rax+8]. # 从内存地址 'x' 下载值并放到edx(寄存器) mov dword ptr [rax+0xc], 1 # 把值 1 上传到内存地址 'y'

因此,由于CPU认为这些指令是独立的,因此在y更新之前先读取x,同理在Test1方法也是会发生x更新之前先读取y。
所以才会出现本文例子的坑~~!

如何解决?

输入内存屏障 内存屏障是对CPU的一种特殊锁定指令,它禁止指令在该屏障上重新排序。因此,该程序将按预期方式运行,但缺点是会慢几十纳秒。

在我们的示例中,注释了一行代码:

//Interlocked.MemoryBarrierProcessWide();

如果取消注释该行,程序将正常运行~~~~~

总结

平常我们说volatile一般很容易去理解它的内存可见性,很难理解内存屏障这个概念,内存屏障的概念中对于volatile变量的赋值,
volatile并不禁止在完成上一条上传指令之前完成对volatile变量的下载指令。这个在多线程环境下一定得注意!

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zwwfyw.html