CPU硬件有它自己的内存模型,不同的编程语言也有它自己的内存模型。

在 C# 的语言规范中 ECMA-334,对于Volatile关键字的描述:

15.5.4 Volatile fields
When a field-declaration includes a
volatile modifier, the fields introduced by that declaration are
volatile fields. For non-volatile fields, optimization techniques that
reorder instructions can lead to unexpected and unpredictable results in
multi-threaded programs that access fields without synchronization such
as that provided by the lock-statement (§13.13). These optimizations
can be
performed by the compiler, by the run-time system, or by
hardware. For volatile fields, such reordering optimizations are
restricted:

  • A read of a volatile field is called a volatile read. A volatile
    read has “acquire semantics”; that is, it is guaranteed to occur prior
    to any references to memory that occur after it in the instruction
    sequence.
  • A write of a volatile field is called a volatile write. A
    volatile write has “release semantics”; that is, it is guaranteed to
    happen after any memory references prior to the write instruction in the
    instruction sequence.

简单来说,对于常规字段,由于代码优化而导致指令顺序改变,如果没有进行一定的同步控制,在多线程应用中可能会导致意想不到的结果,而造成这种意外的原因可能是编译器优化、运行时系统的优化或者因为硬件的原因(即CPU和主存储器的通信模型)。可变(volatile)字段会限制这种优化的发生,在这里引入两个定义:

可变读: 对于可变字段的读操作会获取语义。即,其可以保证对于可变字段的内存读取操作一定发生在其后内存操作指令的前面。进一步解释,与 Thread.MemoryBarrier 类似,获取语义会保证在读取可变字段指令前的指令可以跨越它出现在它后面,但是相反地,在它后面的指令不能跨越它出现在它的前面。例子:

class Volatile_class
{
private int _a;
private volatile int _b;
private int _c; private void Call()
{
int temp=_a;
//由于_b是可变字段,这样可以保证编译器不会将temp2=_c的指令提前到其之前
//但是,可以将temp=_a提到其之后
int temp1=_b;
int temp2=_c; ...
} private void OtherCall(){...}
}
  • 可变写: 对于可变字段的写操作会释放语义。即,其可以保证对于可变字段的写操作发生在其前面指令执行之后,但是在它之后的指令可以跨域它提前执行。

    X86_X64

    现代的 x86_x64 CPU 可以保证字段的读写都是 “volatile” 的,即你不会读取到旧的字段值,这是由 CPU 提供保证的。这样看起来好像与上面的描述存在矛盾,如果 CPU 可以保证所有字段的读写都是 volatile ,那为什么还需要在语言层面提供volatile关键字。其实这是两个不同的概念,CPU 从硬件层面上保证了对内存的读写是实时的,你不会读取到 Stale Value ,无论这个字段是常规字段还是可变字段。而语言层面上的 volatile 只是一个关键字,告诉编译器不能对该字段进行 instruction reorder 等可能导致多线程读写出现不符合预期结果的优化(暂且这样理解)。

参考这段代码:

class Program
{
class infinity_loop
{
public bool Terminated;
} static void Main(string[] args)
{
var loop=new infinity_loop(); new Thread(()=>{
loop.Terminated=true;
}).Start(); while(!loop.Terminated);
}
}

使用 dotnet core Release 模式运行这段代码,可以发现它永远也不会退出,分析汇编代码:

可以看到红色框选位置,指令test一直在比较eax寄存器上的值,而该寄存器缓存了loop对象的Terminated值(为false),汇编语言中,test是对两个参数进行AND操作,并设置对应的标志位。例如,如果两个值的AND操作为0,则ZF标志会被设置为1。而je指令是:根据特定标志位的情况进行跳转,其中就包括了ZF标志位。回到上面的汇编代码,可以知道 test eax eax 肯定会将ZF设置为1,则je就会导致死循环的产生。

尝试为Terminated值添加volatile关键字

class Program
{
class infinity_loop
{
public volatile bool Terminated;//可变字段
} static void Main(string[] args)
{
var loop=new infinity_loop(); new Thread(()=>{
loop.Terminated=true;
}).Start(); while(!loop.Terminated);
}
}

运行代码,可以发现程序正常退出。再看汇编代码:

可以看到,这次是直接比较内存中字段的真实值,而不是寄存器上的值,这样循环会正常退出。

这是因为Loop Hoisting优化策略导致其中的循环判断经过JIT编译器优化后变成如下:

if(!loop.Terminated)
while(true);

可以想象,这段优化过后的代码在多线程应用中是永远不会退出的。

最佳实践

volatile 是一个比较晦涩,理解起来可能比较困难的概念,并不建议在不理解的情况下使用,你可以使用lock,Thread.MemoryBarrier或者Interlocked作为替代,不仅仅因为其中有过多的细节对开发人员隐藏,而且还要保证你的团队组员都理解其中的工作原理,特别地,volatile还会受不同环境影响,例如.NET Framework,编译器版本,甚至是硬件实现,这些都是需要考虑的因素。你要在使用 lock(或者其他)导致的性能开销和 volatile 引入导致的代码维护难度这两方面进行权衡。

最新文章

  1. UVALive 4426 Blast the Enemy! --求多边形重心
  2. seaJS 简单例子,理解seaJS
  3. 【CoreData】 简单地使用
  4. linux 下远程连接windows
  5. 038. asp.netWeb用户控件之六实现日期选择的用户控件
  6. 使用jquery再次封装ajax
  7. jQuery .on() 绑定事件无效
  8. operator.itemgetter的用法【转】
  9. CSS构造表格
  10. 基于动态库的C++插件开发模型
  11. vb delphi7、2010 csharp vb.net空白测试程序
  12. Android 常用 adb 命令总结
  13. 搭建SSH环境之添加所需jar包
  14. Easyui 让Window弹出居中
  15. PCA主成分分析方法
  16. java集合的操作(set,Iterator)
  17. Unity 游戏框架搭建 (三) MonoBehaviour单例的模板
  18. 我的C语言编程风格
  19. Codeforces Round 504
  20. 抽屉柜式MCC柜中PROFIBUS设备推荐波特率及相应传输距离

热门文章

  1. sqoop如何指定pg库的模式
  2. IoC容器-Bean管理注解方式(注入属性@Autowired和Qualifier)
  3. lambda表达式的学习
  4. WebAssembly环境搭建
  5. 微信 CLI 工具正式发布 v1.0
  6. 在终端或idea编译工具中的terminal中运行mvn install 失败
  7. AtCoder AGC003 简要题解
  8. AT2650 [ARC077C] guruguru
  9. 通过json动态创建控制器
  10. JspSmartUpload 简略中文API文档