C# 基础回顾: volatile 关键字
有些人可能从来没看到过这个关键字,这也难怪,因为这个关键字并不常用。那这个关键字到底有什么用呢?
我在网上搜索这个关键字的时候,发现很多朋友都有一个错误的认识 ------ 认为这个关键字可以防止并发争用(有点类似 lock 的赶脚)。
volatile 作用重定义
volatile 中文解释是“可变的”,MSDN 上关于此关键字的解释如下:“volatile 关键字指示一个字段可以由多个同时执行的线程修改。 声明为 volatile 的字段不受编译器优化(假定由单个线程访问)的限制。 这样可以确保该字段在任何时间呈现的都是最新的值。”
不知道你看了上述描述是不是恍然大悟,反正我是没看懂。在网上查阅了众多资料后,才算有所明白,把上面的话用新的方式重新解读后,就有了如下的结论。
1、阻止编译器优化:JIT 编译器会自动对代码进行优化,从而导致最终代码的指令顺序发生变化。使用 volatile 关键字就可以避免 JIT 编译器对此进行优化,如:
public bool _goOn = true; //未优化
public void Execute()
{
while(_goOn)
{
//do something
}
} //优化后
public void ExecuteOptimized
{
if(_goOn)
{
while(true)
{
//do something
}
}
}
上面的方法只是拿来举个例子,实际优化后的情况并不完全一样。
因为 JIT 认为在单个线程内,_goOn 这个变量的值并没有在循环中修改,所以不需要每次重新去读取,因此就会把这个值提取出来。但是如果在循环的时候,有另一个线程修改了 _goOn 的值,那逻辑就会出现错误。
C++ 中的 volatile 关键字无法保证指令的顺序执行
2、阻止处理器优化:对于多线程尤其是多核心的 CPU 来说,当两个线程操作同一个变量,其中一个在不断的读取这个变量,另一个在不断修改这个变量,CPU 会为了减少对内存的大量访问,而将这个变量缓存在多个核的 Cache 中,这样每次执行指令都可以从 Cache 中迅速返回(访问高速缓存的速度要远高于访问内存的速度)。这样虽然性能提高了,但了伴随着一个问题就是,其中一个线程无法立刻收到另一个线程对该变量的更新。使用 volatile 关键字可以确保每次对变量的读取和更新都是直接操作内存,也就是说每个线程所获取到的值都是相同的,不会有冲突。
volatile 运行效果
在 Stackoverflow 上,有个朋友给出了一个可以运行的 volatile 实例,通过这个实例就能更直观的知道 volatile 的作用。
static void Main()
{
var test = new Test(); new Thread(delegate() { Thread.Sleep(); test.foo = ; }).Start(); while (true)
{
if (test.foo == )
{
break;
}
};
Console.WriteLine("OK");
}
根据那位朋友给出的运行方案,我在 release 模式下,使用 Ctrl + F5 直接运行得到的输出是:
等待许多,仍然没有任何输出
修改 foo 的修饰符,加上 volatile,然后再运行:
本机的运行环境为:Win7 x64、Visual Studio 2012。
之所以在不用 volatile 关键字修饰的时候会导致死循环,就是因为指令被优化了。不同的 CPU 架构采用的方式会有所不同,在我的机器上(x64)上,通过查看运行时的汇编指令时可以发现在没有使用 volatile 的情况下,在判断 test.foo == 255 这句话的时候,一直是在读取 EAX 寄存器中的值。而当使用了 volatile 关键字后,每次都是重新从内在中读取。
// 没有使用 volatile 的情况
0000004f mov eax,dword ptr [esi+4] // 读取内存中的值,并保存在寄存器 EAX 中(esi 指向内存中的地址)
00000052 mov eax,dword ptr [eax+4]
00000055 cmp eax,0FFh // 直接比较寄存器 EAX 的值是否为 255
0000005a jne 00000055 // 如果判断不成立,则继续执行上一行代码 // 使用了 volatile 的情况
0000004f mov eax,dword ptr [esi+4] // 读取内存中的值,并保存在寄存器 EAX 中
00000052 cmp dword ptr [eax+4],0FFh // 比较寄存器 EAX 的值是否为 255
00000059 jne 0000004F // 如果判断不成立,则继续执行地址为 4f 的代码
当没有 volatile 修饰时,执行循环的线程只读取了一次 foo 值,然后一直使用该值,造成了死循环。而使用 volatile 后,每次都会去查看最新的 foo 值,因此才能正常执行。
寄存器知识拾遗:多核 CPU 中,每个核心都有全套寄存器。一个线程只可能在一个核心上运行,不可能开始的时候在核心 A 上,结束时却在核心 B 上,这意味着一个线程在其生命周期内只可能操作一套寄存器。而当同一个核心上的不同线程切换时,当前CPU的寄存器值会被保存到线程内核对象的一个上下文结构中,然后下次该线程被再次调度时,会用内核对象中保存的值恢复寄存器。
volatile 不能替代 lock
从上述提到的两点,应该不难看出 volatile 关键字的作用中并没有哪一点是用于避免多线程对同一个变量的争用的,也就是说它不具有同步的作用。
先来看一个示例:
static int i = ; static void Main(string[] args)
{
Task t = Task.Factory.StartNew(() =>
{
i = ;
//Thread.Sleep(500);
Console.WriteLine("10 i={0}", i);
});
Task t2 = Task.Factory.StartNew(() =>
{
i = ;
//Thread.Sleep(1);
Console.WriteLine("100 i={0}", i);
}); Console.ReadLine();
}
10 i=100上述程序运行后,除了主线程,还会创建两个新线程,且都会修改同一个变量。由于无法控制每个线程执行的时机,上述代码运行的结果有可能如下(把注释掉的代码反注释回来,效果更明显):
100 i=100
这就需要同步机制。修改上述代码,加上 lock 看下效果:
static object lckObj = new object();
static int i = ; static void Main(string[] args)
{ Task t = Task.Factory.StartNew(() =>
{
lock (lckObj)
{
i = ;
//Thread.Sleep(500);
Console.WriteLine("10 Thread.Id:{0} i={1}", Thread.CurrentThread.ManagedThreadId, i);
}
});
Task t2 = Task.Factory.StartNew(() =>
{
lock (lckObj)
{
i = ;
//Thread.Sleep(1);
Console.WriteLine("100 Thread.Id:{0} i={1}", Thread.CurrentThread.ManagedThreadId, i);
}
}); Console.ReadLine();
}
10 i=10现在,无论运行上述代码多少次,得的答案都是一样的:
100 i=100
现在,再使用 volatile 看看,是否有同步的效果:
static volatile int i = ; static void Main(string[] args)
{
Task t = Task.Factory.StartNew(() =>
{
i = ;
//Thread.Sleep(500);
Console.WriteLine("10 i={0}", i);
});
Task t2 = Task.Factory.StartNew(() =>
{
i = ;
//Thread.Sleep(1);
Console.WriteLine("100 i={0}", i);
}); Console.ReadLine();
}
运行后,你便会发现,屏幕上显示的输出和没有使用 lock 是完全一样的。
什么时候使用 volatile?
x86 和 x64 架构的 CPU 本身已经对指令的顺序进行了严格的约束,除了各别情况,大多数情况下使用和不使用 volatile 的效果是一样的。
As it happens, Intel’s X86 and X64 processors always apply acquire-fences to reads and release-fences to writes — whether or not you use the volatile keyword — so this keyword has no effect on the hardware if you’re using these processors. However, volatile does have an effect on optimizations performed by the compiler and the CLR — as well as on 64-bit AMD and (to a greater extent) Itanium processors. This means that you cannot be more relaxed by virtue of your clients running a particular type of CPU.
上面的文字大致意思是指 X86 和 X64 的处理器总是会加入内存屏障来防止乱序,所以加不加 volatile 效果一样。但是在诸如 64位的 AMD CPU 或者 Itanium CPU 则需要手动去预防可能的乱序。
lock 关键字会隐式提供内存屏障,且更严格(完全禁止乱序和缓存,而 volatile 只是禁止一部分的乱序,这样编译器仍然可以在一定程度上进行代码优化),在性能上要差于 volatile。因此,除非你非常在意性能,同时对内存模型或CPU平台非常了解,否则建议直接使用 lock 关键字,lock 关键字不止屏蔽了乱序和缓存可能引起的异常,同时也可以避免多个线程的争用。
The following implicitly generate full fences: C#'s lock statement (Monitor.Enter/Monitor.Exit)、All methods on the Interlocked class (we’ll cover these soon) ...
volatile is used to create a memory barrier* between reads and writes on the variable. lock, when used, causes memory barriers to be created around the block inside the lock, in addition to limiting access to the block to one thread.
--- Stackoverflow
修改 <volatile 运行效果> 这一节中的示例,使用 lock 关键字,如:
int foo;
static object lckObj = new object(); static void Main()
{
var test = new Program(); new Thread(delegate()
{
Thread.Sleep();
lock (lckObj)
test.foo = ;
}).Start(); while (true)
{
lock (lckObj)
if (test.foo == )
{
break;
}
}
Console.WriteLine("OK");
}
上述代码运行效果与使用了 volatile 关键字的效果一样。
参考资源
Don't get C# volatile the wrong way
Volatile fields in .NET: A look inside
Volatile vs. Interlocked vs. lock
转载至 http://blog.chenxu.me/post/detail?id=1d39c8ae-4ed7-4498-8408-9ef3a71ed954
C# 基础回顾: volatile 关键字的更多相关文章
- 并发编程基础之volatile关键字的用法
一:概念 volatile关键字是一个轻量级的线程同步,它可以保证线程之间对于共享变量的同步,假设有两个线程a和b, 它们都可以访问一个成员变量,当a修改成员变量的值的时候,要保证b也能够取得成员变量 ...
- java基础系列--volatile关键字
原创作品,可以转载,但是请标注出处地址:http://www.cnblogs.com/V1haoge/p/7833881.html 1.volatile简述 据说,volatile是java语言中最轻 ...
- JAVA多线程基础学习三:volatile关键字
Java的volatile关键字在JDK源码中经常出现,但是对它的认识只是停留在共享变量上,今天来谈谈volatile关键字. volatile,从字面上说是易变的.不稳定的,事实上,也确实如此,这个 ...
- Volatile关键字回顾之线程可见性
java中,volatile关键字有两大作用: 1.保证线程的可见性 2.防止指令重排序 这篇文章主要通过典型案例,体现可见性这一特性. 概念: java中,堆内存是线程共享的.而每个线程,都应该有自 ...
- [C#] C# 基础回顾 - 匿名方法
C# 基础回顾 - 匿名方法 目录 简介 匿名方法的参数使用范围 委托示例 简介 在 C# 2.0 之前的版本中,我们创建委托的唯一形式 -- 命名方法. 而 C# 2.0 -- 引进了匿名方法,在 ...
- Javascript基础回顾 之(二) 作用域
本来是要继续由浅入深表达式系列最后一篇的,但是最近团队突然就忙起来了,从来没有过的忙!不过喜欢表达式的朋友请放心,已经在写了:) 在工作当中发现大家对Javascript的一些基本原理普遍存在这里或者 ...
- Javascript基础回顾 之(一) 类型
本来是要继续由浅入深表达式系列最后一篇的,但是最近团队突然就忙起来了,从来没有过的忙!不过喜欢表达式的朋友请放心,已经在写了:) 在工作当中发现大家对Javascript的一些基本原理普遍存在这里或者 ...
- JavaScript 基础回顾——对象
JavaScript是基于对象的解释性语言,全部数据都是对象.在 JavaScript 中并没有 class 的概念,但是可以通过对象和类的模拟来实现面向对象编程. 1.对象 在JavaScript中 ...
- C#基础回顾:正则表达式
C#基础回顾:正则表达式 写在前面:本文根据笔者的学习体会结合相关书籍资料对正则表达式的语法和使用(C#)进行基本的介绍.适用于初学者. 摘要:正则表达式(Regular Expressions),相 ...
随机推荐
- 手撕面试官系列(八):分布式通讯ActiveMQ+RabbitMQ+Kafka面试专题
ActiveMQ专题 (面试题+答案领取方式见主页) 什么是 ActiveMQ? ActiveMQ 服务器宕机怎么办? 丢消息怎么办? 持久化消息非常慢. 消息的不均匀消费. 死信队列. Active ...
- redis三种集群策略
主从复制 主数据库可以进行读写操作,当读写操作导致数据变化时会自动将数据同步给从数据库 从数据库一般都是只读的,并且接收主数据库同步过来的数据 一个master可以拥有多个slave,但是一个slav ...
- 转:什么是DIP、IoC、DI
DIP依赖倒置原则DIP(Dependency-Inversion Principles) IoC控制反转(Inversion of Control,IoC),简言之就是代码的控制器交由系统控制,而不 ...
- Gulp 给所有静态文件引用加版本号
在juqery和easyui 盛行的年代许多项目采用纯静态页面去构建前端框架从而实现前后端分离的目的.项目开发周期内往往会频繁修改更新某个文件,当你将文件更新到服务器后客户端由于缓存问题而出现显示异常 ...
- Kubernetes(K8s)基础知识(docker容器技术)
今天谈谈K8s基础知识关键词: 一个目标:容器操作:两地三中心:四层服务发现:五种Pod共享资源:六个CNI常用插件:七层负载均衡:八种隔离维度:九个网络模型原则:十类IP地址:百级产品线:千级物理机 ...
- 所有子模块都要执行的checkstyle检查
<!-- 所有子模块都要执行的checkstyle检查 --> <plugin> <groupId>org.apache.maven.plugins</gro ...
- python ocr中文识别库 tesseract安装及问题处理
这个破东西,折腾了快1个小时,网上的教材太乱了. 我解决的主要是windows的问题 先下载exe.(一看到这个,我就有种预感,不妙) https://digi.bib.uni-mannheim.de ...
- JAVA9之后废弃newInstance()方法
JAVA9之后废弃newInstance()方法 根据JAVA11的API 我们可以看见反射中的newInstance()方法不推荐使用了,用 clazz.getDeclaredConstructor ...
- Java 之 设计模式——代理模式
设计模式——代理模式 一.概述 1.代理模式 (1)真实对象:被代理的对象 (2)代理对象:代理真实对象的 (3)代理模式:代理对象代理真实对象,达到增强真实对象功能的目的 二.实现方式 1.静态代理 ...
- tensorflow遇到ImportError: Could not find 'cudart64_100.dll'错误解决
在安装tensorflow的时候,使用import tensorflow出现了找不到dll文件的错误,参考了很多博客和stackflow的解决方案,发现其中只说了版本号不匹配,但是没有具体说明什么样的 ...