关于C#执行顺序带来的一些潜在问题

 更新时间:2020年08月16日 10:43:10   作者:UWP爱好者  
这篇文章主要给大家介绍了关于C#执行顺序带来的一些潜在问题,文中通过示例代码介绍的非常详细,对大家学习或者使用C#具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧

前言

编写程序的时候,人们的直观感觉通常认为,程序的执行顺序是按照语句的顺序进行的。然而,许多编程语言的规范是允许实际执行顺序与语句编写顺序不符的。实际上,编译器为了完成某种优化,常常会对一些操作进行适当的顺序调整,导致一些预料之外的现象。

实验现象

首先,通过一个例子来展示这个现象。在一个C# .NET Core 3.1命令行程序中,定义两个全局变量a和b,在线程1中,依次对b和a进行递增。这样,在任何时刻b应当等于a或a+1。

    static int a = 0;
    static int b = 0;

    static void Thread1()
    {
      while (true)
      {
        ++b;
        ++a;
      }
    }

在线程2中,先读取a的值,然后执行一些其他操作,再读取b的值。如果语句一定是按顺序执行的,那么读取到的b的值应当比读取到的a的值更新,从而b必然大于或等于a(除非b发生了溢出)。编写程序,当b < a时输出它们的值。

  static int c = 0;

  static void Thread2()
  {
    while (true)
    {
      c += b;
      var localA = a;
      c += b;
      var localB = b;
      if (localA > localB)
      {
        Console.WriteLine($"a={localA} b={localB}");
      }
    }
  }

再编写主程序,启动上述的两个线程。

    static void Main(string[] args)
    {
      Task.Run(Thread1);
      Task.Run(Thread2);

      Console.ReadKey();
    }

使用Debug配置,编译并运行该程序,命令行是没有输出的,符合我们的预期。但是使用Release配置的话,就会出现大量输出,其中a的值比b大1到5不等。

查看反汇编可以看到,在第1个c += b语句处,程序将b的值放到了寄存器中,而后面的语句均使用了该寄存器内存放的值。所以,编译器实际上将对b的读取操作合并并且前置了。以下为反汇编结果片段。

00007FFB628A394D mov     rcx,7FFB6292FBD0h 
00007FFB628A3957 mov     edx,1 
00007FFB628A395C call    00007FFBC2387B10 
00007FFB628A3961 mov     esi,dword ptr [7FFB6292FC08h] 
00007FFB628A3967 mov     ecx,esi 
00007FFB628A3969 add     ecx,dword ptr [7FFB6292FC0Ch] 
00007FFB628A396F mov     dword ptr [7FFB6292FC0Ch],ecx 
        var localA = a;
00007FFB628A3975 mov     edi,dword ptr [7FFB6292FC04h] 
        c += b;
00007FFB628A397B add     ecx,esi 
        c += b;
00007FFB628A397D mov     dword ptr [7FFB6292FC0Ch],ecx 
        if (localA > localB)
00007FFB628A3983 cmp     edi,esi 
00007FFB628A3985 jle     00007FFB628A394D 

理论分析

在C#语言标准的Basic concepts一章Execution order一节(参见:Basic concepts – C# language specification)中,提到了C#的执行顺序规范。C#程序的副作用在以下关键点处的顺序是被保留的:

  • 对volatile字段的读写
  • lock语句
  • 线程的创建和终结

C#程序的执行顺序在满足以下条件的情况下,可以由执行环境任意调整的:

  • 在同一线程内,数据的的依赖关系是被保留的。即,结果与语句按照顺序执行的情况一致。
  • 初始化顺序的规则是被保留的。
  • 相对于volatile字段的读写,副作用的顺序是被保留的。

而上述的副作用包括:

  • 读取或写入volatile字段
  • 写入非volatile变量
  • 写入外部资源
  • 抛出异常

由此可以推出,C#程序中对非volatile变量的读取顺序可能会被调整。在只有一个线程对该变量进行操作时,这个顺序的调整是保证不会影响结果的;但如果同时有其他的线程正在对变量进行修改,则读取的顺序是无法确定的。

因此,如果有多个线程同时访问的,对值的实时性有要求的变量,应当设置为volatile变量。将上述实验中的静态变量a和b改为volatile变量后,即使是Release配置下,也不会出现命令行的输出,即两个变量的读取顺序符合原始的语句顺序。

结论

在C#程序中,读取非volatile变量的顺序可能被执行环境任意调整。如果某个变量在被读取的时候会被其他线程写入,为了该读取结果的实时性,应当将该变量设置为volatile变量。

总结

到此这篇关于关于C#执行顺序带来的一些潜在问题就介绍到这了,更多相关C#执行顺序潜在问题内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:

相关文章

  • C#中List集合使用Max()方法查找到最大值的实例

    C#中List集合使用Max()方法查找到最大值的实例

    这篇文章主要介绍了C#中List集合使用Max()方法查找到最大值的实例,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-12-12
  • C#并行编程之Task同步机制

    C#并行编程之Task同步机制

    这篇文章介绍了C#并行编程之Task同步机制,文中通过示例代码介绍的非常详细。对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-05-05
  • C#微信开发之启用开发者模式

    C#微信开发之启用开发者模式

    本文主要介绍了C#微信开发中启用开发者模式的步骤与方法,具有一定的参考价值,下面跟着小编一起来看下吧
    2017-02-02
  • C#实现Ruby的负数索引器

    C#实现Ruby的负数索引器

    这篇文章主要介绍了C#实现Ruby的负数索引器的相关代码和使用方法,非常简单实用,需要的朋友可以参考下
    2016-07-07
  • C#操作windows注册表的方法

    C#操作windows注册表的方法

    这篇文章主要介绍了C#操作windows注册表的方法,涉及C#针对注册表的读取与写入技巧,非常具有实用价值,需要的朋友可以参考下
    2015-04-04
  • C#实现在线更新软件

    C#实现在线更新软件

    winform程序相对web程序而言,功能更强大,编程更方便,但软件更新却相当麻烦,要到客户端一台一台地升级,面对这个实际问题,在最近的一个小项目中,本人设计了一个通过软件实现自动升级技术方案,弥补了这一缺陷,有较好的参考价值
    2015-05-05
  • c#使用filesystemwatcher实时监控文件目录的添加和删除

    c#使用filesystemwatcher实时监控文件目录的添加和删除

    本文主要描述如何通过c#实现实时监控文件目录下的变化,包括文件和目录的添加,删除,修改和重命名等操作
    2014-01-01
  • C#编写一个网游客户端的完整步骤

    C#编写一个网游客户端的完整步骤

    这篇文章主要给大家介绍了关于C#编写一个网游客户端的相关资料,文中通过示例代码以及图文介绍的非常详细,对大家学习或者使用C#具有一定的参考学习价值,需要的朋友可以参考下
    2021-11-11
  • C#中遍历Hashtable的4种方法

    C#中遍历Hashtable的4种方法

    这篇文章主要介绍了C#中遍历Hashtable的4种方法,本文直接给出实例代码,需要的朋友可以参考下
    2015-06-06
  • C#将HashTable中键列表或值列表复制到一维数组的方法

    C#将HashTable中键列表或值列表复制到一维数组的方法

    这篇文章主要介绍了C#将HashTable中键列表或值列表复制到一维数组中方法,涉及C#操作HashTable的相关技巧,需要的朋友可以参考下
    2015-04-04

最新评论