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

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

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

在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)

上面我说道:在实际运行中可以确定某些“上载”和“下载”操作(指令)不会互相影响,

这里有一个前提条件哈:该假设仅基于基于线程的依赖性检查进行(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的另外一个特性:内存屏障

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

  2. 确保在执行对​​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变量的下载指令。这个在多线程环境下一定得注意!

volatile的内存屏障的坑的更多相关文章

  1. volatile 和 内存屏障

    接下来看看volatile是如何解决上面两个问题的: 被volatile修饰的变量在编译成字节码文件时会多个lock指令,该指令在执行过程中会生成相应的内存屏障,以此来解决可见性跟重排序的问题. 内存 ...

  2. synchronized 与 volatile 原理 —— 内存屏障的重要实践

    单例模式的双重校验锁的实现: 第一种: private static Singleton _instance; public static synchronized Singleton getInst ...

  3. C和C++中的volatile、内存屏障和CPU缓存一致性协议MESI

    目录 1. 前言2 2. 结论2 3. volatile应用场景3 4. 内存屏障(Memory Barrier)4 5. setjmp和longjmp4 1) 结果1(非优化编译:g++ -g -o ...

  4. 【C#】通过一个案例 彻底了解 Volatile和 内存屏障

    案例如下的.我个人理解是不会出现出现0,0的结果,但是很明显出现了. 说明对我对 Volatile\内存屏障\乱序排序的理解是不对. 今天就通过这个案例,理清这些概念. using System; u ...

  5. JUC源码学习笔记4——原子类,CAS,Volatile内存屏障,缓存伪共享与UnSafe相关方法

    JUC源码学习笔记4--原子类,CAS,Volatile内存屏障,缓存伪共享与UnSafe相关方法 volatile的原理和内存屏障参考<Java并发编程的艺术> 原子类源码基于JDK8 ...

  6. volatile关键字?MESI协议?指令重排?内存屏障?这都是啥玩意

    一.摘要 三级缓存,MESI缓存一致性协议,指令重排,内存屏障,JMM,volatile.单拿一个出来,想必大家对这些概念应该有一定了解.但是这些东西有什么必然的联系,或者他们之间究竟有什么前世今生想 ...

  7. java内存屏障

    为什么会有内存屏障 每个CPU都会有自己的缓存(有的甚至L1,L2,L3),缓存的目的就是为了提高性能,避免每次都要向内存取.但是这样的弊端也很明显:不能实时的和内存发生信息交换,分在不同CPU执行的 ...

  8. 内存屏障在CPU、JVM、JDK中的实现

    前言 内存屏障(英语:Memory barrier),也称内存栅栏,内存栅障,屏障指令等,是一类同步屏障指令,它使得 CPU 或编译器在对内存进行操作的时候, 严格按照一定的顺序来执行, 也就是说在内 ...

  9. [面试]volatile类型修饰符/内存屏障/处理器缓存

    volatile类型修饰符 本篇文章的目的是为了自己梳理面试知识点, 在这里做一下笔记. 绝大部分内容是基于这些文章的内容进行了copy+整理: 1. http://www.infoq.com/cn/ ...

随机推荐

  1. 前端PDF文件转图片方法

    第一步:先下载pdfjs,网址:PDF下载地址,再引入到项目中,我是标签直接引用的 <script src="pdfjs/build/pdf.js"></scri ...

  2. uni-app开发经验分享七: 有关列表数据下拉加载方法的解析及记录

    在使用uni.request获取后台数据时,我们往往碰到一个问题,列表的懒加载及数据实时更新,这里记录下我制作这类功能的方法. 问题描述:后台返回数据,前端需要进行10个为一组来分页,先显示前10个, ...

  3. 阿里 Mock 工具正式开源,干掉市面上所有 Mock 工具!

    最近栈长注意到阿里开源了自家的 Mock 工具:TestableMock,该工具号称最轻量.简单.舒适的 Mock 测试工具,功能十分强大,媲美 PowerMock,用法比 Mockito 还要简洁, ...

  4. 转 10 jmeter之动态关联

    10 jmeter之动态关联   jmeter中关联是通过之前请求的后置处理器实现的,具体有两种方式:XPath Extractor(一般xml的时候用的多)和正则表达式提取器. 以webtours登 ...

  5. Ansible自动化运维工具的使用

                                 Ansible自动化运维工具的使用       host lnventory 管理主机 ip  root账号密码 ssh端口 core mod ...

  6. pycharm创建文件夹以及查看源文件存放位置(FOR MAC)

    1.首先我们打开pycharm软件,一般首次打开界面如下 2.我们点击creat new project,点击后效果如下,此时创建相应的文件夹 3.创建完成后如下,并可以查看存放文件夹的位置 4.完成 ...

  7. LOJ2632

    题目描述 译自 BalticOI 2011 Day1 T3「Switch the Lamp On」有一种正方形的电路元件,在它的两组相对顶点中,有一组会用导线连接起来,另一组则不会.有  个这样的元件 ...

  8. pikachu靶场XSS详解

    一.反射型XSS 1.get型 源码前后区别 前 <form method="get"> <input class="xssr_in" typ ...

  9. Typora使用与GItHhub图床配置

    Typora使用 (windows) 1 快捷键 1.1 表格 快捷方式:CTRL+T ID name year 1 Oracle 10 2 Mysql 10 3 Postgresql 20 1.2 ...

  10. Language Guide (proto3) | proto3 语言指南(一)定义消息类型

    定义消息类型 首先让我们看一个非常简单的例子.假设您想定义一个搜索请求消息格式,其中每个搜索请求都有一个查询字符串.您感兴趣的特定结果页以及每页的结果数.下面是用于定义.proto消息类型的文件. s ...