首页 > 编程语言 >C# Volatile

C# Volatile

时间:2022-11-12 19:33:53浏览次数:68  
标签:Console parent C# public 线程 Volatile volatile juster

原文网址:​https://zhuanlan.zhihu.com/p/433007630

1.Overview

经常研究.NET源码库的小伙伴会经常看到一个关键字volatile,那它在开发当中的作用是什么呢?

我们一起来看看官方文档里是怎么描述的,如下:

volatile 关键字指示一个字段可以由多个同时执行的线程修改。出于性能原因,编译器,运行时系统甚至硬件都可能重新排列对存储器位置的读取和写入。声明为 volatile 的字段将从某些类型的优化中排除。不确保从所有执行线程整体来看时所有易失性写入操作均按执行顺序排序。”

本文将围绕这部分进行解读。

声明语法如下:

class VolatileTest
{
   public volatile int sharedStorage;
   
   public void Test(int i)
  {
       sharedStorage = i;
  }
}

2.Detail

我们先了解一下前置知识点。

(1)在CLR中将对sbytebyteshortushortintuintcharfloat 和 bool。以及引用类型保证读写时原子性的(long、double不是原子性读写)变量中的所有字节都是一次性写入或读取的。

(2)Framework Class Library(FCL) 保证所有静态方法都是线程安全的。这意味着假如两个线程同时调用一个静态方法,不会有数据被损坏。为什么?

public static string Print(String str)
{
   string val = "";
   val += str;
   return val;
}

因为静态方法内声明的变量,每个线程调用时都会新创建一份,而不会共用一个存储单元。比如这里的val每个线程都会创建自己的一份,因此不会有线程安全问题。注意:静态变量,由于是在类加载时占用一个存储区每个线程都是共用这个存储区的,所以如果在静态方法里使用了静态变量;这就会有线程安全问题。

(3)内存、CPU缓存(注:下列为简述内容,实际上不仅如此)

CPU缓存,CPU集成的缓存。

内存,内存条硬件提供的存储空间。


我们继续回到主要内容上,用下面的若干代码示例来表达volatile的作用。

public class Program
{
       public static int bookNum = 0;

       public static void Main(string[] args)
      {
           Console.WriteLine("juster书的数量:" + bookNum);

           Thread juster = new Thread(() =>
          {
               Console.WriteLine("juster没带书,等待家长送书到学校...");

               while (bookNum == 0) {}

               Console.WriteLine("juster拿到书,开始上课听讲。");
          });
           juster.Name = nameof(juster);
           juster.Start();

           Thread parent = new Thread(() =>
          {
               Console.WriteLine("parent在屋里找书中...");

               Thread.Sleep(2000);

               Console.WriteLine("parent找到了书之后,送往学校...");

               SendBook();
          });
           parent.Name = nameof(parent);
           parent.Start();
      }

       public static void SendBook()
      {
           bookNum = 1;
      }
}

代码执行输出如下:

 

 

这时候诡异的来了,按照正常的代码执行逻辑不难看出当parent线程执行Sendbook()的时候juster应该就能拿到书上课了。但是这里juster却一直没有拿到是为什么呢?心细的小伙伴应该观察到了这里的运行模式是Release,众所周知Release是.Net的发布版本执行效率会比Debug版本要高。

为什么Release版本效率高呢?怎么得来的?下面这段代码来解释:

上面这张反编译的图不难看出,10*10-100这段代码直接编译成0了。这种现象是因为Release编译的时候编译器会对代码进行‘优化’。这段是最直观能看到的‘优化’效果,其实C#编译器将你的代码转换成中间语言(IL)。然后,JIT将IL转换成本机CPU指令。此外,C#编译器、JIT编译器,甚至CPU本身都可能优化你的代码。

但是实际上在上述代码中count的值始终为0;所以循环永远不会执行,没有必要编译循环内的代码在编译后会被‘优化’。说了这么多,只是为了给大伙证明Release编译这一层会存在‘优化’;接下来继续回到volatile上。

说到这里,如何解决各种‘优化’带来的问题呢?这时候只需要在booknum前面加上volatile关键字修饰即可。

public class Program
{
       public static volatile int bookNum = 0;

       public static void Main(string[] args)
      {
           Console.WriteLine("juster书的数量:" + bookNum);

           Thread juster = new Thread(() =>
          {
               Console.WriteLine("juster没带书,等待家长送书到学校...");

               while (bookNum == 0) { }

               Console.WriteLine("juster拿到书,开始上课听讲。");
          });
           juster.Name = nameof(juster);
           juster.Start();


           Thread parent = new Thread(() =>
          {
               Console.WriteLine("parent在屋里找书中...");

               Thread.Sleep(2000);

               Console.WriteLine("parent找到了书之后,送往学校...");

               SendBook();
          });
           parent.Name = nameof(parent);
           parent.Start();
      }

       public static void SendBook()
      {
           bookNum = 1;
      }
}

 

 

在被各种优化之后,booknum因为是值类型在每个线程访问时会发生复制且又是在静态方法中被修改。所以每个线程都会复制booknum的值到当前线程上下文中缓存起来。这样就导致了parent线程修改了booknum的值juster线程看不到的情况。这个时候就需要用volatile关键字告诉编译器不需要这样的优化,表示用volatile定义的变量会被改变,每次都必须从内存中读取,而不能把他放在CPU cache或寄存器中重复使用。最后booknum会在运行的过程中修改值且其他线程能‘共享访问’达到最终的效果。

3.Conclusion

Part1

volatile 关键字可应用于以下类型的字段:

  • 引用类型。
  • 指针类型(在不安全的上下文中)。请注意,虽然指针本身可以是可变的,但是它指向的对象不能是可变的。换句话说,不能声明“指向可变对象的指针”。
  • 简单类型,如 sbytebyteshortushortintuintcharfloat 和 bool
  • 具有以下基本类型之一的 enum 类型:bytesbyteshortushortint 或 uint
  • 已知为引用类型的泛型类型参数。
  • IntPtr 和 UIntPtr。

其他类型(包括 double 和 long)无法标记为 volatile,因为对这些类型的字段的读取和写入不能保证是原子的。若要保护对这些类型字段的多线程访问,请使用 Interlocked 类成员或使用 lock 语句保护访问权限。

volatile 关键字只能应用于 class 或 struct 的字段。不能将局部变量声明为 volatile

Part2

volatile并不能用来做线程同步,它的主要作用时为了让多个线程之间能看到被修改过后最新的值。

 

 

Part3

C#不支持以传递引用的方式将volatile字段传给方法。

int.TryParse("123", out x);

Part4

除了禁止编译优化,还有同步到内存中因为CPU每个核心都有自己Cache所以需要同步到内存中方便其他核心使用。

Part5

看完本文也能解开小白时期的疑惑,为什么我写代码编译成release版本之后就不能运行报错的奇特现象了。

Part6

volatile 牵扯到的相关知识点和原理远远不止这些。

4.Reference

https://docs.microsoft.com/zh-c

标签:Console,parent,C#,public,线程,Volatile,volatile,juster
From: https://www.cnblogs.com/bruce1992/p/16884479.html

相关文章

  • Centos7下yum安装apache2.4和php7.3
    因不同版本间存在少许差异,先说明环境版本:Centos7.8、Apache2.4、PHP7.3,亲测成功参考安装:https://blog.csdn.net/qq_35145723/article/details/109811593参考配置:https://......
  • 各种CPU的ELF编码,ELF并没有为龙芯分配253-256
    关于龙芯公布的ELF编码邮件ReservedELFmachinenumbersEM_LS253toEM_LS256>>>Hi,>>>>>>IreservedELFmachinenumbers253-256forLoongson.>>>>>>The255......
  • Linux的Anaconda换阿里源
    简介Anaconda是一个用于科学计算的Python发行版,支持Linux,Mac,Windows,包含了众多流行的科学计算、数据分析的Python包。下载地址:​​https://mirrors.aliyun.com/anacond......
  • Type-challenges题目
    Type-challenges题目13HelloWorld就是类型别名,​​Helloworld​​​就是​​string​​的别名。类似于cpp里的​​typedef​​​,语法是​​type[名称]=[数据类型]​​......
  • 2006 An AES smart card implementation resistant to power analysis attacks
    一、对DPA攻击的反制措施1掩码定义:所有中间值通过一个随机值(掩码)m隐藏起来,该掩码由密码设备内部生成,通常与中间值进行异或原理:由于掩码是随机的且对攻击者未知,被掩......
  • CentOS7.x下在后台运行和关闭(Java)项目
    需求在一般情况下,在服务器通过java-jarxxx.jar来运行一个jar包。但是如果退出了控制台,那么这个程序就将被关闭。因此让jar包后台运行十分必要。解决方案运行方式一......
  • [游记]CSP-S2022退役记
    图在笔记本里,找时间传上来Day-710.22早上收拾东西准备滚去隔离一个大行李箱里面半箱吃的,被舍友怒斥资本主义开展了滑箱子比赛,我和\(\color{black}{c}\color{red}{rs......
  • nacos核心概念一篇速过
    地域物理的数据中心,资源创建成功后不能更换。可用区同一地域内,电力和网络互相独立的物理区域。同一可用区内,实例的网络延迟较低。接入点地域的某个服务的入口域名。命名空间......
  • Android TCP客户端
    文章目录​​一、创建工程​​​​二、添加网络权限​​​​三、添加布局代码​​​​四、添加逻辑代码​​​​五、通信测试​​​​六、源码分享​​一、创建工程二、添加......
  • CodeForces-1005#C
    正文将原式\(a_i+a_j=2^p\)转化为\(a_j=2^p-a_i\),对于,每个\(a_i\),枚举\(p\),可以有效地降低时间复杂度。设\(num\leftarrow0\),若\(2^p-a_i\)存在相等的\(a_j\),......