volatile的内存屏障的坑

请看下面的代码并尝试猜测输出:

可能一看下面的代码你可能会放弃继续看了,但如果你想要彻底弄明白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的许多输出,如下图。

image

我们先根据代码自我分析下

在Test1中,我们将x设置为1,将a设置为y,而Test2将y设置为1,将b设置为x
因此这4条语句会在2个线程中竞争
罗列下可能会发生的几种情况:

1. Test1先于Test2执行: x = 1 a = y y = 1 b = x

在这种情况下,我们假设Test1在Test2之前完成,那么最终值将是

x = 1,a = 0,y = 1,b = 1 2. Test2执行完成后执行Test1: y = 1 b = x x = 1 a = y

在这种情况下,那么最终值将是

x = 1,a = 1,y = 1,b = 0 2. Test1执行期间执行Test2: x = 1 y = 1 b = x a = y

在这种情况下,那么最终值将是

x = 1,a = 1,y = 1,b = 1 3. Test2执行期间执行Test1 y = 1 x = 1 a = y b = x

在这种情况下,那么最终值将是

x = 1,a = 1,y = 1,b = 1 4. Test1交织Test2 x = 1 y = 1 a = y b = x

在这种情况下,那么最终值将是

x = 1,a = 1,y = 1,b = 1 5.Test2交织Test1 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服务上载或从中下载一样慢。

以下是各项指标(2020年数据)(ns为纳秒)

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缓存中的内容慢100倍

如果让你开发一个应用程序,实现上载或者下载功能。
您将如何设计此?肯定想要开多线程,并行化执行以节省时间!
这正是CPU的功能。CPU被我们设计的很聪明,
在实际运行中可以确定某些“上载”和“下载”操作(指令)不会互相影响,
并且CPU为了节省时间,对它们(指令)进行了(优化)并行处理,
也叫【cpu乱序执行】(out-of-order)

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

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