C/C++のvolatileキーワード

5124 ワード

以下に説明するvolatileキーワードはC/C++言語のみのものであり、他の言語には適用する.
volitate
volatileとして定義された変数は、コンパイラがこの変数の値を最適化しないように、この変数が予想外に変更される可能性があるということです.正確には、オプティマイザは、レジスタに保存されたバックアップではなく、この変数を使用するたびに、この変数の値を慎重に再読み取りする必要があります.volatile変数のいくつかの例を次に示します.
  • ハードウェアレジスタ(ステータスレジスタなど)
  • 割り込みサービスサブルーチンでアクセスされる非自動変数(Non-automatic variables)
  • マルチスレッドアプリケーションでいくつかのタスクによって共有される変数
  • マルチスレッドのプログラムでは、共通にアクセスするメモリの中で、複数のプログラムがこの変数を操作することができ、あなた自身のプログラムはいつこの変数が変化するかを判定できません.例えば、Aスレッドが変数をレジスタにコピーする、ループに入ってレジスタの値が一定の条件(Bスレッドが変数の値を変更することを期待する)を満たすか否かを繰り返し検出すると、Bは変数の値を変更するが、この変更はAスレッドがレジスタにコピーした値には影響せず、Aは常にデッドループ状態となる.
    に質問
  • パラメータはconstでもvolatileでもいいですか?なぜか説明する.答え:はい.1つの例は、読み取り専用のステータスレジスタである.それはvolatileです.予想外に変わる可能性があるからです.これはconstです.プログラムはそれを修正しようとしないからです.
  • ポインタはvolatileですか?なぜか説明する.はい、結構です.これはあまりよくありませんが.1つの例は、割り込みサービスサブルーチンがbufferを指すポインタを変更する場合である.
  • 次の関数にエラーがあります.
  •      int square(volatile int *ptr)
             {
                  return *ptr * *ptr;
             } 
    

    このコードの目的は、ポインタ*ptrが値の平方を指すことですが、*ptrがvolatile型のパラメータを指すため、コンパイラは次のようなコードを生成します.
      int square(volatile int *ptr) 
        {
             int a,b;
             a = *ptr;
             b = *ptr;
             return a * b;
         } 
    

    *ptrの値は予想外に変更される可能性があるため、aとbは同じ値ではない可能性があります.その結果、このコードはあなたが望んでいる平方値ではないことを返す可能性があります.正しいコードは次のとおりです.
        long square(volatile int *ptr) 
         {
                int a;
                a = *ptr;
                return a * a;
         }
    

    volatileで宣言する変数の値を使用することを要求すると、システムは常にそのメモリからデータを読み出し直し、前の命令がその場所からデータを読み出したばかりで、読み出したデータがすぐに保存される.volatile int i = 10; int a=i; ...// , , i int b =i; volatileは、iがいつでも変化する可能性があることを指摘し、それを使用するたびにiのアドレスから読み出さなければならないため、コンパイラが生成したアセンブリコードは、iのアドレスからデータを読み直してbに置く.最適化の方法は、コンパイラがiからデータを読み出すコード間のコードがiを操作していないことを2回発見したため、前回読んだデータをbに自動的に配置することである.iから読み直すのではなく.これにより、iがレジスタ変数であるか、ポートデータを表すとエラーが発生しやすいため、volatileは特殊なアドレスへの安定したアクセスを保証することができる.
    もう1つの例は、for(int i =0; i<100000;i++)という文は空のサイクルの速度をテストするために使用されるが、コンパイラはそれを最適化し、実行しないに違いない.しかし、for(volatile int i=0;i<100000;i++);と書くと彼は実行します.
    マルチスレッドデータ同期における役割
    ロックされていない共有データ
    ロックを使用しない場合は、次の例のように、複数のスレッドが共有するデータの使用に注意する必要があります.
    int gCounter; 
    void Increment(void) { gCounter++; } 
    int GetCurrent(void) { return gCounter; } 
    

    複数のスレッドが同時にIncrementを呼び出すと、gCounter++が3段階の原子操作に分割されるため、セキュリティは保証されません.
  • 現在値をレジスタ
  • に読み込む.
  • レジスタ自増
  • レジスタの値をメモリ
  • に書き込む
    volatileは上記のような状況を解決することができない.次のコードを考慮します.
    struct SharedDataStructure gSharedStructure; 
    int gFlag;
    
    //  A
    gSharedStructure.foo = ...; 
    gSharedStructure.bar = ...; 
    gSharedStructure.baz = ...; 
    gFlag = 1; 
    ...
    
    //  B
     if(gFlag) 
            UseSharedStructure(&gSharedStructure;); 
    ...
    

    上記のコードでは、構造体とflagの間に依存関係がないため、実行順序がコンパイラによって変更される可能性がある.スレッドAのコードは、flagが先に1に設定され、その後構造体が処理されるため、スレッドBのif文内は安全ではない可能性があります.この場合、gFlagとgSharedStructureにvolatileキーワードを付けて、コンパイラがコード順にマシンコードを生成することを保証することができます.
    CPU Memory Reorderingは、volatileキーワードを追加した後も、上記のコードは実際の実行時に安全を保証することはできません.CPUは急進的なreorderで実行速度を速めるため、内部でマシンコードを実行する順序は依然として未知であり、最終終了時の結果が元の順序と一致することを保証するだけです(as-if).この場合、スレッドAとスレッドBが2つのCPU実行に割り当てられている場合、BのCPUは、実際には、AのCPU実行状況を知らない.したがって、gFlagが先に修正され、gSharedStructureが処理されていないという問題は依然として存在する.この場合、OSMemoryBarrier(libkern/OSatomic.h)を使用して問題を解決できます.コードの変更は次のとおりです.
    //  A
    gSharedStructure.foo = ...; 
    gSharedStructure.bar = ...; 
    gSharedStructure.baz = ...; 
    OSMemoryBarrier(); 
    gFlag = 1; 
    ...
    
    //  B
    if(gFlag) { 
            OSMemoryBarrier(); 
            UseSharedStructure(&gSharedStructure;); 
        } 
    

    これはgSharedStructureです.volatileを追加する必要はありません.gFlagには具体的な状況があります.
    while(1) { 
            if(gFlag) { 
                OSMemoryBarrier(); 
                UseSharedStructure(&gSharedStructure;); 
            } 
        } 
    

    上記の場合、volatileを追加する必要があります.このコードセグメントではgFlagが変更されていないため、コンパイラはgFlag値を1回だけキャッシュに入れて使用しています.
    別の状況を見てみましょう.
      - (void)method { 
            if(gFlag) { 
                OSMemoryBarrier(); 
                UseSharedStructure(&gSharedStructure;); 
            } 
        } 
    

    この場合、gFlagにvolatileを追加する必要はありません.コードは外部から呼び出されるたびにgFlagを再読み込みします.(inlineまたは全体的な最適化によりforeign codeではない可能性があることに注意してください).
    volatileの別の例を使用する必要があります.
    int gCount; 
        // Thread A: 
        while(!done) { 
            work(); 
            gCount++; 
        } 
        // Thread B: 
        while(gCount < total) ;
    

    ただし、volatile int 64と宣言されている場合は例外があります.t gCountは32-bit CPU上で実際に動作し、CPUは一度の原子操作で変数の読み取り/書き込みを完了できないため、安全を保証できない.最後に、volatileの様々な特性を熟知するも、実際には使用を推奨しない.様々なコンパイラのオプティマイザにバグがある可能性があるからである.
    結論:マルチスレッドでvolatileを使用してデータの同期問題を解決すると予想しないでください.このロック時にロックします.
  • volatileは、foreign code
  • がループに存在しない場合、共有値をループで読み書きする際に非常に有用である.
  • volatileはマルチセグメントコードの実行順序を有効に保証することができず、OSMemoryBarrierを適用して
  • を解決する.
  • volatileマルチスレッドシーンでは、CPUによって原子読み書きができない変数には使用されない(32 bit/64 bit)
  • volatileは、ロックまたは他の原子操作スキームを有する複雑なタイプの共有データに対して
  • を使用しない.
  • コードがvolatileを完璧に使用しても、コンパイラがバグを出さないことは保証されません.