SoFunction
Updated on 2025-03-07

Some potential problems caused by C# execution order

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!