C#执行顺序潜在问题 关于C#执行顺序带来的一些潜在问题
UWP爱好者 人气:0前言
编写程序的时候,人们的直观感觉通常认为,程序的执行顺序是按照语句的顺序进行的。然而,许多编程语言的规范是允许实际执行顺序与语句编写顺序不符的。实际上,编译器为了完成某种优化,常常会对一些操作进行适当的顺序调整,导致一些预料之外的现象。
实验现象
首先,通过一个例子来展示这个现象。在一个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变量。
总结
加载全部内容