1. 程式人生 > 程式設計 >關於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#執行順序潛在問題內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!