Preface
When writing a program, people's intuitive feeling usually believes that the execution order of the program is carried out in the order of statements. However, the specifications of many programming languages allow the actual execution order to be inconsistent with the order of statement writing. In fact, in order to complete some optimization, the compiler often adjusts some operations in appropriate order, resulting in some unexpected phenomena.
Experimental phenomenon
First, use an example to demonstrate this phenomenon. In a C# .NET Core 3.1 command line program, two global variables a and b are defined, and in thread 1, incrementing b and a in turn. In this way, at any time b should be equal to a or a+1.
static int a = 0; static int b = 0; static void Thread1() { while (true) { ++b; ++a; } }
In thread 2, first read the value of a, then perform some other operations, and then read the value of b. If the statement must be executed in order, the value of b read should be updated than the value of a read, so b must be greater than or equal to a (unless b overflow occurs). Write a program that outputs their values when b < a.
static int c = 0; static void Thread2() { while (true) { c += b; var localA = a; c += b; var localB = b; if (localA > localB) { ($"a={localA} b={localB}"); } } }
Write the main program and start the above two threads.
static void Main(string[] args) { (Thread1); (Thread2); (); }
Using Debug configuration, compile and run the program, the command line has no output, which is in line with our expectations. However, if you use Release configuration, a large number of outputs will appear, where the value of a ranges from 1 to 5 larger than b.
Looking at the disassembly, you can see that at the first c += b statement, the program puts the value of b in the register, and the subsequent statements use the values stored in the register. So, the compiler actually merges and prefixes the read operations to b. The following is a fragment of the disassembly result.
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
Theoretical analysis
In the Basic concepts chapter of C# language standard, the Execution order section (see:Basic concepts – C# language specification) The execution order specification of C# is mentioned. The order in which side effects of C# programs are retained at the following key points:
- Read and write to volatile fields
- lock statement
- Creation and ending of threads
The execution order of C# programs can be adjusted arbitrarily by the execution environment if the following conditions are met:
- In the same thread, the dependencies of the data are preserved. That is, the result is consistent with the case where the statement is executed in order.
- Rules of initialization order are reserved.
- The order of side effects is preserved relative to reading and writing of the volatile field.
The above side effects include:
- Read or write to volatile fields
- Write non-volatile variables
- Write to external resources
- throw an exception
From this, it can be introduced that the order of reading non-volatile variables in C# programs may be adjusted. When only one thread operates on the variable, the adjustment of this order is ensured that it will not affect the result; but if other threads are modifying the variable at the same time, the order of reading cannot be determined.
Therefore, if multiple threads access simultaneously, variables that require real-time value should be set to volatile variables. After changing the static variables a and b in the above experiment to volatile variables, even under Release configuration, the command line output will not appear, that is, the reading order of the two variables conforms to the original statement order.
in conclusion
In C# programs, the order in which non-volatile variables are read may be arbitrary to be adjusted by the environment. If a variable is written by other threads when it is read, the variable should be set as a volatile variable for the real-time nature of the read result.
Summarize
This is all about this article about some potential problems caused by C# execution order. For more related content on potential problems of C# execution order, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!