有些人可能从来没看到过这个关键字,这也难怪,因为这个关键字并不常用。那这个关键字到底有什么用呢?

我在网上搜索这个关键字的时候,发现很多朋友都有一个错误的认识 ------ 认为这个关键字可以防止并发争用(有点类似 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.

Nonblocking Synchronization

上面的文字大致意思是指 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) ...

Nonblocking Synchronization

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

Nonblocking Synchronization

C/C++ Volatile关键词深度剖析

转载至 http://blog.chenxu.me/post/detail?id=1d39c8ae-4ed7-4498-8408-9ef3a71ed954

C# 基础回顾: volatile 关键字的更多相关文章

  1. 并发编程基础之volatile关键字的用法

    一:概念 volatile关键字是一个轻量级的线程同步,它可以保证线程之间对于共享变量的同步,假设有两个线程a和b, 它们都可以访问一个成员变量,当a修改成员变量的值的时候,要保证b也能够取得成员变量 ...

  2. java基础系列--volatile关键字

    原创作品,可以转载,但是请标注出处地址:http://www.cnblogs.com/V1haoge/p/7833881.html 1.volatile简述 据说,volatile是java语言中最轻 ...

  3. JAVA多线程基础学习三:volatile关键字

    Java的volatile关键字在JDK源码中经常出现,但是对它的认识只是停留在共享变量上,今天来谈谈volatile关键字. volatile,从字面上说是易变的.不稳定的,事实上,也确实如此,这个 ...

  4. Volatile关键字回顾之线程可见性

    java中,volatile关键字有两大作用: 1.保证线程的可见性 2.防止指令重排序 这篇文章主要通过典型案例,体现可见性这一特性. 概念: java中,堆内存是线程共享的.而每个线程,都应该有自 ...

  5. [C#] C# 基础回顾 - 匿名方法

    C# 基础回顾 - 匿名方法 目录 简介 匿名方法的参数使用范围 委托示例 简介 在 C# 2.0 之前的版本中,我们创建委托的唯一形式 -- 命名方法. 而 C# 2.0 -- 引进了匿名方法,在 ...

  6. Javascript基础回顾 之(二) 作用域

    本来是要继续由浅入深表达式系列最后一篇的,但是最近团队突然就忙起来了,从来没有过的忙!不过喜欢表达式的朋友请放心,已经在写了:) 在工作当中发现大家对Javascript的一些基本原理普遍存在这里或者 ...

  7. Javascript基础回顾 之(一) 类型

    本来是要继续由浅入深表达式系列最后一篇的,但是最近团队突然就忙起来了,从来没有过的忙!不过喜欢表达式的朋友请放心,已经在写了:) 在工作当中发现大家对Javascript的一些基本原理普遍存在这里或者 ...

  8. JavaScript 基础回顾——对象

    JavaScript是基于对象的解释性语言,全部数据都是对象.在 JavaScript 中并没有 class 的概念,但是可以通过对象和类的模拟来实现面向对象编程. 1.对象 在JavaScript中 ...

  9. C#基础回顾:正则表达式

    C#基础回顾:正则表达式 写在前面:本文根据笔者的学习体会结合相关书籍资料对正则表达式的语法和使用(C#)进行基本的介绍.适用于初学者. 摘要:正则表达式(Regular Expressions),相 ...

随机推荐

  1. PHP中YUM的理解

    1. YUM是什么? 1)全称:Yellow dog Updater ,Modified. 2)百度简述:是一个在Fedora和RedHat以及CentOS中的Shell前端软件包管理器.基于RPM包 ...

  2. idea远程部署SpringBoot项目到Docker

    安装docker服务或者系统学习docker参考这篇文档:https://shimo.im/docs/fE0eJCx8IIojQXzB/ 1.配置docker的远程端口 vim /usr/lib/sy ...

  3. Windows 上的应用程序在运行期间可以给自己改名(可以做 OTA 自我更新)

    原文:Windows 上的应用程序在运行期间可以给自己改名(可以做 OTA 自我更新) 程序如何自己更新自己呢?你可能会想到启动一个新的程序或者脚本来更新自己.然而 Windows 操作系统允许一个应 ...

  4. 谈谈MySQL中的锁

    谈谈MySQL中的锁 锁的定义 ​ 在生活中锁的例子就非常多了,所以应该很容易理解锁的含义.在计算机领域,可以这样来概述,锁是计算机协调多个进行进程并发访问某一资源的机制. ​ 在数据库中,锁也是一个 ...

  5. Node和浏览器端所支持的模块规范不同

  6. nodeJS从入门到进阶二(网络部分)

    一.网络服务器 1.http状态码 1xx: 表示普通请求,没有特殊含义 2xx:请求成功 200:请求成功 3xx:表示重定向 301 永久重定向 302 临时重定向 303 使用缓存(服务器没有更 ...

  7. js文本对象模型[DOM]【续】(Node节点类型)

    一.Document类型    document实例1.常用的一些属性documentElement 始终指向HTML页面中的<html>元素.body 直接指向<body>元 ...

  8. Java服务端口被占用问题

    在改code的时候eclipse突然崩溃了,未响应状态等了好久也没转完,只能结束进程了,再次打开eclipse果然无法启动项目.报的错误是端口被占用. 又不想重启电脑,只能记录下微服务下的卡死清理端口 ...

  9. oracle中start with和connect by的用法理解

    转自:https://blog.csdn.net/qq_29274091/article/details/72627350 Oracle中start with和connect by 用法理解转自:ht ...

  10. nepenthes用法

    安装 # apt-get install nepenthes 配置文件 # vi submit-file.conf submit-file { path "/var/lib/nepenthe ...