上面我说道:在实际运行中可以确定某些“上载”和“下载”操作(指令)不会互相影响,
这里有一个前提条件哈:该假设仅基于基于线程的依赖性检查进行(per-thread basis dependency checks)。
虽然在单个线程是可以被确定为指令独立性,但CPU无法考虑多个线程的情况,所以提供了【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代码:
留意我打了注释的那行。上面的这些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 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变量的下载指令。这个在多线程环境下一定得注意!