C#でvolatileキーワードを使用する場合

共通言語ランタイムのJIT(ジャストインタイム)コンパイラで使用される最適化手法は、.Netプログラムがマルチスレッドシナリオでデータの不揮発性読み取りを実行しようとすると、予測できない結果をもたらす可能性があります。この記事では、揮発性メモリアクセスと不揮発性メモリアクセスの違い、C#でのvolatileキーワードの役割、およびvolatileキーワードの使用方法について説明します。

概念を説明するために、C#でいくつかのコード例を提供します。volatileキーワードがどのように機能するかを理解するには、まず、.NetでJITコンパイラ最適化戦略がどのように機能するかを理解する必要があります。

JITコンパイラの最適化を理解する

JITコンパイラーは、最適化戦略の一部として、プログラムの意味と最終的な出力を変更しない方法で、読み取りと書き込みの順序を変更することに注意してください。これは、以下のコードスニペットに示されています。

x = 0;

x = 1;

上記のコードスニペットは、プログラムの元のセマンティクスを保持しながら、次のように変更できます。

x = 1;

JITコンパイラは、「定数伝播」と呼ばれる概念を適用して、次のコードを最適化することもできます。

x = 1;

y = x;

上記のコードスニペットは、プログラムの元のセマンティクスを維持しながら、次のように変更できます。

x = 1;

y = 1;

揮発性メモリアクセスと不揮発性メモリアクセス

現代のシステムのメモリモデルは非常に複雑です。プロセッサレジスタ、さまざまなレベルのキャッシュ、および複数のプロセッサで共有されるメインメモリがあります。プログラムが実行されると、プロセッサはデータをキャッシュし、実行中のスレッドから要求されたときにキャッシュからこのデータにアクセスする場合があります。このデータの更新と読み取りは、キャッシュされたバージョンのデータに対して実行される可能性がありますが、メインメモリは後の時点で更新されます。このメモリ使用量のモデルは、マルチスレッドアプリケーションに影響を及ぼします。 

1つのスレッドがキャッシュ内のデータと対話していて、2番目のスレッドが同じデータを同時に読み取ろうとすると、2番目のスレッドがメインメモリから古いバージョンのデータを読み取る可能性があります。これは、不揮発性オブジェクトの値が更新されると、メインメモリではなく実行中のスレッドのキャッシュで変更が行われるためです。ただし、揮発性オブジェクトの値が更新されると、実行中のスレッドのキャッシュに変更が加えられるだけでなく、このキャッシュがメインメモリにフラッシュされます。また、揮発性オブジェクトの値が読み取られると、スレッドはキャッシュを更新し、更新された値を読み取ります。

C#でvolatileキーワードを使用する

C#のvolatileキーワードは、オペレーティングシステム、ハードウェア、または同時に実行されているスレッドによって変数の値が変更される可能性があるため、変数の値をキャッシュしてはならないことをJITコンパイラに通知するために使用されます。したがって、コンパイラは、データの競合、つまり変数の異なる値にアクセスする異なるスレッドにつながる可能性のある変数の最適化の使用を回避します。

オブジェクトまたは変数を揮発性としてマークすると、揮発性の読み取りおよび書き込みの候補になります。C#では、データを揮発性オブジェクトに書き込むか不揮発性オブジェクトに書き込むかに関係なく、すべてのメモリ書き込みが揮発性であることに注意してください。ただし、データを読み取っているときにあいまいさが発生します。不揮発性のデータを読み取る場合、実行中のスレッドは常に最新の値を取得する場合としない場合があります。オブジェクトが揮発性の場合、スレッドは常に最新の値を取得します。

volatileキーワードを前に付けることで、変数を揮発性として宣言できます。次のコードスニペットはこれを示しています。

クラスプログラム

    {{

        public volatile int i;

        static void Main(string [] args)

        {{

            //ここにコードを記述します

        }

    }

volatileキーワードは、任意の参照、ポインター、および列挙型で使用できます。また、byte、short、int、char、float、およびboolタイプでvolatile修飾子を使用することもできます。ローカル変数は揮発性として宣言できないことに注意してください。参照型オブジェクトをvolatileとして指定すると、インスタンスの値ではなく、ポインター(オブジェクトが実際に格納されているメモリ内の場所を指す32ビット整数)のみが揮発します。また、double変数は64ビットのサイズであり、x86システムのワードサイズよりも大きいため、揮発性にすることはできません。double変数を揮発性にする必要がある場合は、クラス内でラップする必要があります。以下のコードスニペットに示すようにラッパークラスを作成することで、これを簡単に行うことができます。

パブリッククラスVolatileDoubleDemo

{{

    プライベート揮発性WrappedVolatileDoublevolatileData;

}

パブリッククラスWrappedVolatileDouble

{{

    public double Data {get; セットする; }

ただし、上記のコード例の制限に注意してください。あなたが最新の値がなければなりませんが、volatileData参照ポインタを、あなたは、最新の値が保証されていないDataプロパティを。この回避WrappedVolatileDouble策は、型を不変にすることです。

volatileキーワードは、特定の状況でのスレッドセーフに役立ちますが、スレッドの同時実行性の問題すべてに対する解決策ではありません。変数またはオブジェクトを揮発性としてマークすることは、lockキーワードを使用する必要がないことを意味しないことを知っておく必要があります。volatileキーワードは、lockキーワードの代わりにはなりません。同じデータにアクセスしようとしている複数のスレッドがある場合に、データの競合を回避するのに役立つだけです。