可能一看下面的代码你可能会放弃继续看了,但若是你想要完全弄明白volatile,你须要耐心,下面的代码很简单!缓存
在下面的代码中,咱们定义了4个字段x,y,a和b,它们被初始化为0
而后,咱们建立2个分别调用Test1和Test2的任务,并等待两个任务完成。
完成两个任务后,咱们检查a和b是否仍为0,
若是是,则打印它们的值。
最后,咱们将全部内容重置为0,而后一次又一次地运行相同的循环。多线程
using System; using System.Threading; using System.Threading.Tasks; namespace MemoryBarriers { class Program { static volatile int x, y, a, b; static void Main() { while (true) { var t1 = Task.Run(Test1); var t2 = Task.Run(Test2); Task.WaitAll(t1, t2); if (a == 0 && b == 0) { Console.WriteLine("{0}, {1}", a, b); } x = y = a = b = 0; } } static void Test1() { x = 1; // Interlocked.MemoryBarrierProcessWide(); a = y; } static void Test2() { y = 1; b = x; } }
若是您运行上述代码(最好在Release模式下运行),则会看到输出为0、0的许多输出,以下图。ide
在Test1中,咱们将x设置为1,将a设置为y,而Test2将y设置为1,将b设置为x
所以这4条语句会在2个线程中竞争
罗列下可能会发生的几种状况:优化
x = 1 a = y y = 1 b = x
在这种状况下,咱们假设Test1在Test2以前完成,那么最终值将是spa
x = 1,a = 0,y = 1,b = 1
y = 1 b = x x = 1 a = y
在这种状况下,那么最终值将是线程
x = 1,a = 1,y = 1,b = 0
x = 1 y = 1 b = x a = y
在这种状况下,那么最终值将是设计
x = 1,a = 1,y = 1,b = 1
y = 1 x = 1 a = y b = x
在这种状况下,那么最终值将是code
x = 1,a = 1,y = 1,b = 1
x = 1 y = 1 a = y b = x
在这种状况下,那么最终值将是blog
x = 1,a = 1,y = 1,b = 1
y = 1 x = 1 b = x a = y
在这种状况下,那么最终值将是排序
x = 1,a = 1,y = 1,b = 1
我认为上面已经罗列的
已经涵盖了全部可能的状况,
可是不管发生哪一种竞争状况,
看起来一旦两个任务都完成,
就不可能使a和b都同时为零,
可是奇迹般地,竟然一直在打印0,0 (请看上面的动图,若是你怀疑的话代码copy执行试试)
先揭晓答案:cpu的乱序执行。
让咱们看一下Test1和Test2的IL中间代码。
我在相关部分中添加了注释。
#ConsoleApp9.Program.Test1() #function prolog ommitted L0015: mov dword ptr [rax+8], 1 # 把值 1 上传到内存地址 'x' L001c: mov edx, [rax+0xc] # 从内存地址 'y' 下载值并放到edx(寄存器) L001f: mov [rax+0x10], edx. # 从(edx)寄存器把值上传到内存地址 'a' L0022: add rsp, 0x28. L0026: ret #ConsoleApp9.Program.Test2() #function prolog L0015: mov dword ptr [rax+0xc], 1 # 把值 1 上传到内存地址 'y' L001c: mov edx, [rax+8]. # 从内存地址 'x' 下载值并放到edx(寄存器) L001f: mov [rax+0x14], edx. # 从(edx)寄存器把值上传到内存地址 'b' L0022: add rsp, 0x28 L0026: ret
请注意,我在注释中使用“上载”和“下载”一词,而不是传统的读/写术语。
为了从变量中读取值并将其分配到另外一个存储位置,
咱们必须将其读取到CPU寄存器(如上面的edx),
而后才能将其分配给目标变量。
因为CPU操做很是快,所以与在CPU中执行的操做相比,对内存的读取或写入真的很慢。
因此我使用“上传”和“下载”,相对于CPU的高速缓存而言【读取和写入内存的行为】
就像咱们向远程Web服务上载或从中下载同样慢。
L1 cache reference: 1 ns
L2 cache reference: 4 ns
Branch mispredict: 3 ns
Mutex lock/unlock: 17 ns
Main memory reference: 100 ns
Compress 1K bytes with Zippy: 2000 ns
Send 2K bytes over commodity network: 44 ns
Read 1 MB sequentially from memory: 3000 ns
Round trip within same datacenter: 500,000 ns
Disk seek: 2,000,000 ns
Read 1 MB sequentially from disk: 825,000 ns
Read 1 MB sequentially from SSD: 49000 ns
若是让你开发一个应用程序,实现上载或者下载功能。
您将如何设计此?确定想要开多线程,并行化执行以节省时间!
这正是CPU的功能。CPU被咱们设计的很聪明,
在实际运行中能够肯定某些“上载”和“下载”操做(指令)不会互相影响,
而且CPU为了节省时间,对它们(指令)进行了(优化)并行处理,
也叫【cpu乱序执行】(out-of-order)
上面我说道:在实际运行中能够肯定某些“上载”和“下载”操做(指令)不会互相影响,
这里有一个前提条件哈:该假设仅基于基于线程的依赖性检查进行(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代码:
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
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变量的下载指令。这个在多线程环境下必定得注意!