编写程序的时候,人们的直观感觉通常认为,程序的执行顺序是按照语句的顺序进行的。然而,许多编程语言的规范是允许实际执行顺序与语句编写顺序不符的。实际上,编译器为了完成某种优化,常常会对一些操作进行适当的顺序调整,导致一些预料之外的现象。
实验现象
首先,通过一个例子来展示这个现象。在一个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变量。
留言
有想法?请给我们留言!您的留言不会直接显示在网站内。