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 引入导致的代码维护难度这两方面进行权衡。

【C# 线程 】内存模型 与Volatile的更多相关文章

  1. 面试时通过volatile关键字,全面展示线程内存模型的能力

    面试时,面试官经常会通过volatile关键字来考核候选人在多线程方面的能力,一旦被问题此类问题,大家可以通过如下的步骤全面这方面的能力.     1 首先通过内存模型说明volatile关键字的作用 ...

  2. java线程内存模型,线程、工作内存、主内存

    转自:http://rainyear.iteye.com/blog/1734311 java线程内存模型 线程.工作内存.主内存三者之间的交互关系图: key edeas 所有线程共享主内存 每个线程 ...

  3. Java内存模型:volatile详解

    详见:http://blog.yemou.net/article/query/info/tytfjhfascvhzxcyt202 Java内存模型:volatile是干什么用的Volatile字段是用 ...

  4. Java并发编程:JMM(Java内存模型)和volatile

    1. 并发编程的3个概念 并发编程时,要想并发程序正确地执行,必须要保证原子性.可见性和有序性.只要有一个没有被保证,就有可能会导致程序运行不正确. 1.1. 原子性 原子性:即一个或多个操作要么全部 ...

  5. 【java】java内存模型(2)--volatile内存语义详解

    多线程并发编程中synchronized和Volatile都扮演着重要的角色,Volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”.可见性的意思是当一个线程 ...

  6. Java内存模型与volatile关键字

    Java内存模型与volatile关键字 一).并发程序开发 并行程序的开发要涉及多线程.多任务间的协作和数据共享问题. 常用的并发控制:内部锁.重入锁.读写锁.信号量. 二).线程的特点 线程的特点 ...

  7. 基础篇:深入JMM内存模型解析volatile、synchronized的内存语义

    目录 1 java内存模型,JMM(JAVA Memory Model) 2 CPU高速缓存.MESI协议 3 指令重排序和内存屏障指令 4 happen-before原则 5 synchronize ...

  8. Java线程内存模型-JVM-底层原理

    public class Demo1 { private static boolean initFlag=false; public static void main(String[] args) t ...

  9. Java并发编程、内存模型与Volatile

    http://www.importnew.com/24082.html  volatile关键字 http://www.importnew.com/16142.html  ConcurrentHash ...

随机推荐

  1. Cesium源码剖析---Post Processing之物体描边(Silhouette)

    Cesium在1.46版本中新增了对整个场景的后期处理(Post Processing)功能,包括模型描边.黑白图.明亮度调整.夜视效果.环境光遮蔽等.对于这么炫酷的功能,我们绝不犹豫,先去翻一翻它的 ...

  2. 【Kafka】基于Windows环境的Kafka有关环境(scala+zookeeper+kafka+可视化工具)搭建、以及使用.NET环境开发的案例代码与演示

    前言:基于Windows系统下的Kafka环境搭建:以及使用.NET 6环境进行开发简单的生产者与消费者的演示. 一.环境部署 Kafka是使用Java语言和Scala语言开发的,所以需要有对应的Ja ...

  3. gin框架简介

    介绍 Gin是一个golang的微框架,封装比较优雅,API友好,源码注释比较明确,具有快速灵活,容错方便等特点 对于golang而言,web框架的依赖要远比Python,Java之类的要小.自身的n ...

  4. openstack,docker,mesos,Kubernetes(k8s)

    作者:张乾链接:https://www.zhihu.com/question/62985699/answer/204233732来源:知乎著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注明 ...

  5. MySQL更新数据时,日志(redo log、binlog)执行流程

    1:背景 项目需要做Es和数据库的同步,而手动在代码中进行数据同步又是Es的一些不必要的数据同步操作和业务逻辑耦合,所以使用的了读取mysql的binlog日志的方式进行同步Es的数据. 问题1:根据 ...

  6. 初识 oracle!

    /** * 一.oracle的简介? * 1.是一个关系型数据库,强大! * * 软件名 开发商 用途 * * oracle oracle 专门的软件公司 收费!1.连接的用户数,2.服务器的cpu的 ...

  7. MySql数据存储格式Compact及计算MySql的B+Tree高度

    1.MySql的compact行记录格式 MySql从版本5.1以后默认使用的是compact行记录格式.可以通过执行以下命令查询到Row_format知悉InnoDB行记录格式类型. show ta ...

  8. JavaCV的摄像头实战之六:保存为mp4文件(有声音)

    欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...

  9. (DDS)正弦波形发生器——幅值、频率、相位可调(一)

    (DDS)正弦波形发生器--幅值.频率.相位可调 一.项目任务: 设计一个幅值.频率.相位均可调的正弦波发生器. 频率每次增加1kHz. 相位每次增加 2*PI/256 幅值每次增加两倍 二.文章内容 ...

  10. RabbitMQ简介及安装

    AMQP简介 AMQP AMQP(Advanced Message Queuing Protocol,高级消息队列协议)是进程之间传递异步消息的网络协议. AMQP工作过程 发布者(Publisher ...