c keyword null
メモリモデルは、ハードウェア、同時実行性、コンパイラの最適化、さらには数学にも触れる魅力的なトピックです
.
メモリモデルは、他のスレッドによって変更されたメモリ位置を読み込んだときにスレッドがどのような状態になるかを定義します. たとえば、あるスレッドが通常の不揮発性フィールドを更新する場合、フィールドを読み取る別のスレッドが新しい値を決して観察しない可能性があります. このプログラムは決して(リリースビルドで)終了しません:
class Test {プライベートブール_ループ=真; public static void Main(){テストtest1 =新しいテスト(); //別のスレッドで_loopをfalseに設定するnew Thread(()=> {test1. _loop = false;}).
c keyword null アカウント
開始(); // _loopフィールドをポーリングしてfalseに設定します(test1. _loop == true); //上記のループは決して終了しません! }}
whileループを終了させるには、次の2つの方法があります。
ロックを使用して_loopフィールドへのすべてのアクセス(読み取りと書き込み)を保護する
_loopフィールドをvolatileとしてマークする
不揮発性フィールドの読み込みが古い値を観測する理由は2つあります。コンパイラの最適化とプロセッサの最適化.
並行プログラミングでは、スレッドはさまざまな方法でインターリーブされ、多分異なる結果をもたらします. しかし、無限ループの例では、ロックや揮発性フィールドを正しく使用しない限り、スレッドはインタリーブされるだけでなく、より複雑な方法で相互作用する可能性があります.
コンパイラの最適化
非揮発性リードが陳腐化した値を返す最初の理由は、コンパイラの最適化と関係がある. 無限ループの例では、JITコンパイラはwhileループを最適化します。
while(test1.
c keyword null キーボード
_loop == true);
これに:
if(test1. _loop){while(true);} }
1つのスレッドのみが_loopフィールドにアクセスする場合、これは完全に合理的な変換です. しかし、別のスレッドがフィールドの値を変更した場合、この最適化によって、読み取りスレッドが更新された値に気づくのを防ぐことができます.
_loopフィールドをvolatileとマークすると、コンパイラはループから読み込みをホイストしません.
コンパイラは、他のスレッドがフィールドを変更している可能性があることを知っているので、無効な値を読み込む最適化を避けるように注意します.
私が示したコード変換は、CLR JITコンパイラによって行われた最適化の近似ですが、完全には正確ではありません.
c keyword null サンプル
JITコンパイラによって生成されたアセンブリコードは、値test1を格納します. EAXレジスタの_loop. ループ条件はレジスタをポーリングし続け、test1. メモリから再び_ループする. スレッドがプリエンプトされている場合でも、CPUレジスタはセーブされます. スレッドの実行が再びスケジュールされると、EAXレジスタの値が失われ、ループは終了しません.
c keyword null 検索
whileループによって生成されたアセンブリコードは、次のようになります。
00000068 test eax、eax 0000006a jne 00000068
_loopフィールドをvolatileにすると、代わりにこのコードが生成されます。
00000064 cmpバイトptr、0 00000068 jne 00000064
_loopフィールドがvolatileでない場合、コンパイラは_loopをEAXレジスタに格納します. _loopがvolatileの場合、コンパイラはtest1変数をEAXに保持し、_loopの値は各アクセス時にメモリから再フェッチされます(ptr).
CLRの現行バージョンでの私の経験から、この種のコンパイラの最適化が非常に頻繁ではないという印象を受けました. x86とx64では、フィールドがvolatileかどうかにかかわらず、同じアセンブリコードが生成されることがよくあります. IA64では、状況は少し異なります。次のセクションを参照してください。.
c keyword null キーボード
プロセッサの最適化
いくつかのプロセッサでは、コンパイラは揮発性の読み書きに対する特定の最適化を避けるだけでなく、特別な命令を使用する必要があります. マルチコアマシンでは、コアごとにキャッシュが異なります. プロセッサは、デフォルトでこれらのキャッシュを一貫性のあるものに保つことはできませんし、キャッシュをフラッシュしてリフレッシュするために特別な指示が必要な場合があります.
主流x86およびx64プロセッサは、メモリアクセスが効果的に揮発性である強力なメモリモデルを実装しています. したがって、揮発性のフィールドは、コンパイラにループからの読み出しを持ち上げるようないくつかの高水準の最適化を避けるように強制しますが、それ以外の場合、不揮発性の読み取りと同じアセンブリコードになります.
Itaniumプロセッサは、より弱いメモリモデルを実装しています.
c keyword null にすることはできません。
Itaniumをターゲットにするには、JITコンパイラは揮発性メモリアクセス用の特別な命令を使用する必要があります。LD. ACQおよびST. LDとSTの代わりにREL. 指示LD. ACQは効果的に言います、私のキャッシュをリフレッシュし、値とSTを読む. RELによると、キャッシュに値を書き込んだ後、キャッシュをメインメモリにフラッシュする .
c keyword null タグ
一方、LDとSTは、他のプロセッサには見えないプロセッサのキャッシュにアクセスするだけです.
このセクションと前のセクションで説明されている理由から、フィールドをvolatileとしてマークすると、x86およびx64ではパフォーマンスペナルティがゼロになることがよくあります.
x86 / x64命令セットには、実際には3つのフェンス命令LFENCE、SFENCE、およびMFENCEが含まれています. 現在のアーキテクチャではLFENCEとSFENCEは必要ないはずですが、MFENCEは特定の問題を回避するのに便利です。コアが以前に書き込んだメモリ位置を読み取った場合、書き込みはストアバッファから処理されますまだメモリに書き込まれている. 実際にCLR JITがMFENCE命令を挿入するかどうかはわかりません.
c keyword null 検索
より奥行きのある揮発性のアクセス
揮発性および不揮発性のメモリアクセスがどのように機能するかを理解するために、各スレッドが独自のキャッシュを持つものと考えることができます. 不揮発性メモリロケーション(i. e. フィールド)u、および揮発性メモリ位置v.
不揮発性の書き込みは、スレッドのキャッシュ内の値を更新するだけで、メインメモリの値を更新することはできません。
しかし、C#ではすべての書き込みが揮発性である(Javaの場合と異なり)、揮発性フィールドと非volatileフィールドのどちらに書き込むかにかかわらず.
c keyword null 結合
だから、上記の状況は実際にはC#.
volatile writeはスレッドのキャッシュを更新し、キャッシュ全体をメインメモリにフラッシュします. volatileフィールドvを11に設定すると、uとvの両方の値がメインメモリにフラッシュされます。
すべてのC#書き込みが揮発性であるため、すべての書き込みがメインメモリにまっすぐ進むと考えることができます.
通常の非揮発性リードは、主メモリからではなく、スレッドのキャッシュから値を読み取ることができます.
c keyword null サンプル
スレッド1が11に設定されているにも関わらず、スレッド2がuを読み込んだ場合でも、値10が表示されます。
C#で不揮発性フィールドを読み取ると、不揮発性の読み取りが行われ、スレッドのキャッシュから古い値が表示されることがあります. または、更新された値が表示されることがあります. 古い値と新しい値のどちらが表示されるかは、コンパイラとプロセッサによって異なります.
最後に、揮発性の読み込みの例を見てみましょう. スレッド2はvolatileフィールドvを読み込みます:
揮発性読み取りの前に、スレッド2はそのキャッシュ全体をリフレッシュし、次に更新されたv:11の値を読み込みます. したがって、実際にメインメモリにある値を観察し、キャッシュをボーナスとしてリフレッシュします.
c keyword null 空文字
私が記述したスレッドキャッシュは架空のものであり、実際にはスレッドキャッシュ. スレッドは、これらのキャッシュをコンパイラ最適化とプロセッサ最適化の成果物としてしか見ない.
1つの興味深い点は、C#でのすべての書き込みは、こことここで文書化されているようにメモリモデルに従って揮発性であり、おそらくはそのように実装されているということです.
c keyword null 空文字
C#言語のECMA仕様では、デフォルトでは書き込みが揮発性でない弱いモデルが実際に定義されています.
揮発性の読み取りが読み取り値だけでなくキャッシュ全体を更新するのは驚くかもしれません. 同様に、揮発性書き込み(i. e. 、すべてのC#書き込み)は、書き込み値だけでなくキャッシュ全体をフラッシュします. これらのセマンティクスは、強力な揮発性セマンティクスと呼ばれることもあります .
1995年に設計されたオリジナルのJavaメモリモデルは、弱い揮発性セマンティクスに基づいていましたが、2004年には大幅に変更されました. 弱い揮発性モデルは非常に不便です.
c keyword null タグ
この問題の1つの例は、安全な発行パターンが安全でないことです. この例を考えてみましょう。
volatile string [] _args = null;
public void Write(){string [] a =新しい文字列= "arg1"; a = "arg2"; _args = a; . . . } public void Read(){if(_args!= null){//弱い揮発性セマンティクスの下では、このアサートは失敗する可能性があります。デバッグ. アサート(_args!= null); }}強い揮発性セマンティクス(i.
c keyword null から文字
e. 、 . NETおよびC#の揮発性セマンティクス)、_argsフィールドのnull以外の値は_argsの要素もnullでないことを保証します. 安全な出版パターンは非常に有用であり、実際には一般的に使用されています.
メモリモデルと . NET操作
ここにどのように様々な . NET操作は仮想スレッドキャッシュと相互作用します。
構築する
スレッドキャッシュをリフレッシュする前に?
後にスレッドキャッシュをフラッシュしますか?
ノート
普通の読書
いいえ
いいえ
不揮発性フィールドの読み取り
通常の書き込み
いいえ
はい
不揮発性フィールドの書き込み
揮発性読み出し
はい
いいえ
揮発性フィールド、またはスレッドの読み取り.
c keyword null アカウント
VolatileRead
揮発性書き込み
いいえ
はい
不揮発性フィールドと同じ不揮発性フィールドの書き込み
糸. MemoryBarrier
はい
はい
特別なメモリバリアメソッド
インターロック操作
はい
はい
インクリメント、追加、交換など.
ロック取得
はい
いいえ
モニター. ロック{}領域を入力または入力する
ロックリリース
いいえ
はい
モニター. ロック{}領域を終了または終了する
各操作について、この表には2つのことが示されています。
操作の前に想像上のスレッドキャッシュ全体がメインメモリからリフレッシュされていますか?
仮想スレッドキャッシュ全体が操作後にメインメモリにフラッシュされますか?
モデルの免責と制限
このブログ記事は私の個人的理解を反映しています . NETメモリモデルであり、公開されている情報のみに基づいています.
c keyword null 文字列
想像上のスレッドキャッシュに基づく説明は、操作の並べ替えに基づいてより一般的に使用される説明よりも直観的であることがわかります. スレッドキャッシュモデルは、ほとんどの目的と目的のためにも正確です.
さらに正確にするには、スレッドキャッシュが任意の大きな階層を形成できると仮定して、読み込みが2つの可能な場所からのみ提供されるとは想定できません。メインメモリまたはスレッドのキャッシュ. 私は、キャッシュ階層が違いを生み出すためには、いくらか巧妙なケースを構築しなければならないと思います. 誰かが、階層スレッドキャッシュモデルが並べ替えベースのモデルとは異なる予測をするケースを知っているなら、それについて聞いてみたいと思います.
あなたが . NETメモリモデルについては、MSDN Magazineのマルチスレッドアプリケーションにおける低ロック技術の影響と、Chris Brummeのメモリモデルのブログ記事を読むことをお勧めします.