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 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}"); } } }メインプログラムを作成し、上記の2つのスレッドを起動します。

    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〓言語標準のBaic concepts一章Execution orderの一節(参照:Baic concepts C〓langage specification)において、C〓〓の実行順序規範が言及されている。Cピラプログラムの副作用は以下のキーポイントの順序で保持されています。
  • によるvolatileフィールドの読み書き
  • ロック文
  • スレッドの作成と終了
  • C((xi)プログラムの実行順序は、以下の条件を満たす場合、実行環境によって任意に調整できます。
  • は同じスレッド内にあり、データの依存関係は保留されている。つまり、結果は文が順番に実行される場合と一致します。
  • 初期化順序の規則は保持されている。
  • は、volatileフィールドの読み書きに対して、副作用の順序が保持されている。
  • 上記の副作用は以下を含む。
  • volatileフィールドを読み込みまたは書き込みする
  • 書き込み非volatile変数
  • 外部リソース
  • を書き込みます。
  • 投げ異常
  • これにより、C〓〓プログラムでは非volatile変数に対する読み取り順序が調整される可能性がある。一つのスレッドだけがこの変数を操作する場合、この順序の調整は結果に影響しないことを保証します。しかし、他のスレッドが変数を修正している場合、読み取りの順序は確定されません。
    したがって、複数のスレッドが同時にアクセスされる場合、値のリアルタイム性に要求される変数は、volatile変数として設定されるべきである。上記の実験における静的変数aとbをvolatile変数に変更すると、Release設定であってもコマンドラインの出力は現れません。つまり、2つの変数の読み取り順序は元のステートメント順序に合致します。
    結論
    C((xi)プログラムでは、非volatile変数を読み込む順序は、環境によって任意に調整される場合があります。ある変数が読み込まれると他のスレッドに書き込まれますが、その読み込み結果のリアルタイム性のために、この変数をvolatile変数に設定します。
    締め括りをつける
    ここでは、Cの実行順に関する潜在的な問題について紹介します。Cの実行順に関する問題がもっと多いので、私たちの以前の文章を検索したり、下記の関連記事を見たりしてください。これからもよろしくお願いします。