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),相 ...
随机推荐
- [转帖]我花了10个小时,写出了这篇K8S架构解析
我花了10个小时,写出了这篇K8S架构解析 https://www.toutiao.com/i6759071724785893891/ 每个微服务通过 Docker 进行发布,随着业务的发展,系统 ...
- JAVA如何实现中式排名和美式排名
根据公司需求,需要编写中式和美式排名算法,根据具体业务编写的,代码如下,看不懂留言,欢迎探讨,求高手指教更高效稳定的方法.private static int[] datas = {9,9,10,10 ...
- Python如何获取系统大小端模式
1. 第一种方法导入sys模块: >>> import sys >>> >>> sys.byteorder 'little' >>&g ...
- 【LEETCODE】64、链表分类,medium&hard级别,题目:2,138,142,23
package y2019.Algorithm.LinkedList.medium; import y2019.Algorithm.LinkedList.ListNode; /** * @Projec ...
- Linux 生成随机mac地址,并固化到本地
前言: 将Mac地址随机化并固化到本地可以有效避免同一个网络内,mac地址冲突导致的网络阻塞问题. 以下是有关的方法: 1.使用$RANDOM和md5sum(嵌入式无需移植其他软件的优秀可选方案) M ...
- emmet 配置文件
snippets.json(添加自己的或更新现有的片段) preferences.json(更改某些Emmet过滤器和操作的行为) SyntaxProfiles.json(定义生成的HTML / XM ...
- [cf 1239 B] The World Is Just a Programming Task (Hard Version)
题意: 给你一个长度为n的括号序列,你可以交换其中的两个元素,需要使该序列的n个循环移位中合法的括号序列个数尽量多. 输出最大的答案以及交换哪两个元素能够取到这个答案. $n\leq 3\times ...
- [开发ing] Unity项目 - Hero英雄
目录 游戏原型 项目演示 绘图资源 代码实现 技术探讨 参考来源 游戏原型 游戏介绍:这是一款横版类魂游戏,玩家将操控Hero,在诸神黄昏的墓地中,挑战源源不断的敌人,以及近乎无敌的强大boss 灵感 ...
- Calico网络模型
由于两台物理机的容器网段不同,我们完全可以将两台物理机配置成为路由器,并按照容器的网段配置路由表. 在物理机A中,我们可以这样配置:要想访问网段172.17.9.0/24,下一跳是192.168.10 ...
- SQL Server 2012启动时提示:无效的许可证数据,需要重新安装
因为手咸,觉得电脑没有VS 2010版本的软件,就把Microsoft Visual C++ 2010某个组件给卸载了. 然后打开Sql Server 2012,就开始报错. 重装之后,也还是报错,将 ...