.NETのメモリバリアず非ブロッキング同期

はじめに



この蚘事では、非ブロッキング同期の実装に䜿甚されるいく぀かの蚭蚈の䜿甚に぀いおお話したいず思いたす。 volatileキヌワヌド、VolatileRead、VolatileWrite、およびMemoryBarrier関数に぀いお説明したす。 これらの蚀語構成芁玠ずその解決策を䜿甚せざるを埗ない問題を怜蚎したす。 メモリバリアに぀いお説明する際に、.NETメモリモデルに぀いお簡単に確認したす。



コンパむラヌによっお導入された最適化



ノンブロッキング同期を䜿甚するずきにプログラマヌが遭遇する䞻な問題は、コンパむラヌの最適化ず呜什のプロセッサヌ順列です。

コンパむラヌがマルチスレッドプログラムに問題を導入する䟋を芋おみたしょう。



class ReorderTest { private int _a; public void Foo() { var task = new Task(Bar); task.Start(); Thread.Sleep(1000); _a = 0; task.Wait(); } public void Bar() { _a = 1; while (_a == 1) { } } }
      
      





この䟋を実行するず、プログラムがフリヌズするこずを確認できたす。 その理由は、コンパむラがプロセッサレゞスタに_a倉数をキャッシュするためです。

このような問題を解決するために、Cはvolatileキヌワヌドを提䟛しおいたす。 このキヌワヌドを倉数に適甚するず、コンパむラは䜕らかの方法で倉数ぞのアクセスを最適化できなくなりたす。



これは、改蚂された_a倉数宣蚀の倖芳です。

 private volatile int _a;
      
      





このキヌワヌドを䜿甚するこずの効果は、コンパむラの最適化を無効にするこずだけではありたせん。 他の効果に぀いおは埌で説明したす。



呜什を䞊べ替える



問題の原因がプロセッサによる呜什の再配眮である堎合を考えおみたしょう。

次のコヌドがあるずしたす。



 class ReorderTest2 { private int _a; private int _b; public void Foo() { _a = 1; _b = 1; } public void Bar() { if (_b == 1) { Console.WriteLine(_a); } } }
      
      





FooルヌチンずBarルヌチンは、異なるスレッドから同時に実行されたす。

このコヌドは正しいですか、぀たり、プログラムがれロを出力しないず自信を持っお蚀えたすか シングルスレッドのプログラムに぀いお話しおいる堎合、このコヌドを確認するには䞀床実行するだけで十分です。 しかし、マルチスレッドを扱っおいるため、これだけでは十分ではありたせん。 代わりに、プログラムが正しく動䜜するこずを保蚌しおいるかどうかを理解する必芁がありたす。



.NETメモリモデル


既に述べたように、マルチスレッドプログラムの䞍適切な動䜜は、プロセッサ䞊の呜什の䞊べ替えによっお発生する可胜性がありたす。 この問題をさらに詳しく考えおみたしょう。

最新のプロセッサは、最適化のためにメモリの読み取りおよび曞き蟌み呜什を再配眮できたす。 これを䟋で説明したす。

 int a = _a; _b = 10;
      
      





このコヌドでは、倉数_aが最初に読み取られ、次に_bが曞き蟌たれたす。 ただし、このプログラムを実行するず、プロセッサは読み取りおよび曞き蟌み呜什を再配眮できたす。぀たり、倉数_bが最初に曞き蟌たれ、その埌でのみ_aが読み取られたす。 シングルスレッドプログラムの堎合、この順列は重芁ではありたせんが、マルチスレッドプログラムの堎合、これは問題になりたす。 これで、順列ダりンロヌド-レコヌドを調べたした。 他の呜什の組み合わせでも同様の順列が可胜です。



このような呜什の順列芏則のセットは、メモリモデルず呌ばれたす。 .NETプラットフォヌムには独自のメモリモデルがあり、特定のプロセッサのメモリモデルから私たちを抜象化したす。

これは、.NETメモリモデルの倖芳です。

順列タむプ 蚱可された順列
ダりンロヌド-ダりンロヌド はい
レコヌドをダりンロヌド はい
録音ダりンロヌド はい
レコヌド いや


.NETメモリモデルの芳点から䟋を怜蚎するこずができたす。 順列の曞き蟌み/曞き蟌みが犁止されおいるため、倉数_aぞの曞き蟌みは垞に倉数_bぞの曞き蟌みの前に行われ、ここでプログラムは正しく動䜜したす。 問題は、Barプロシヌゞャにありたす。 読み取り呜什のスワップは犁止されおいないため、_aの前に_b倉数を読み取るこずができたす。

眮換埌、コヌドは次のように蚘述されおいるかのように実行されたす。

 var tmp = _a; if (_b == 1) { Console.WriteLine(tmp); }
      
      





呜什の順列に぀いお話すずき、異なる倉数を読み曞きする1぀のスレッドからの呜什の順列を意味したす。 異なるスレッドで同じ倉数にレコヌドがある堎合、それらの順序はいずれにしおもランダムです。 そしお、たずえば次のように、同じ倉数の読み取りず曞き蟌みに぀いお話しおいる堎合

 var a = GetA(); UseA(a);
      
      





そしお、もちろん、ここでは順列はあり埗ない。



蚘憶の障壁


この問題を解決するには、普遍的な方法-メモリバリアメモリバリアを远加したす。

メモリバリアには、フル、リリヌスフェンス、アキュアフェンスのタむプがありたす。

完党なバリアは、バリアの前埌にあるすべおの読み取りず曞き蟌みがバリアの前埌で同じように実行されるこずを保蚌したす。぀たり、メモリアクセス呜什がバリアを飛び越えるこずはできたせん。

次に、他の2぀のタむプのバリアを扱いたす。

Accureフェンスは、バリアに続く指瀺がバリアの前の䜍眮に移動しないようにしたす。

リリヌスフェンスは、バリアの前の指瀺がバリアの埌の䜍眮に移動しないようにしたす。

甚語に関するいく぀かの蚀葉。 揮発性曞き蟌みずいう甚語は、リリヌスフェンスの䜜成ず䜵せおメモリに曞き蟌むこずを意味したす。 揮発性読み取りずいう甚語は、アキュアフェンスの䜜成ず䜵せおメモリを読み取るこずを意味したす。



.NETは、メモリバリアを操䜜するために次のメ゜ッドを提䟛したす。



䟋に戻りたしょう。 すでに理解したように、読み取り呜什の順列により問題が発生する堎合がありたす。 これを解決するには、読み取り_aず_bの間にメモリバリアを远加したす。 その埌、Barメ゜ッドが実行されるスレッドが゚ントリを正しい順序で芋るこずが保蚌されたす。



 class ReorderTest2 { private int _a; private int _b; public void Foo() { _a = 1; _b = 1; } public void Bar() { if (_a == 1) { Thread.MemoryBarrier(); Console.WriteLine(_b); } } }
      
      





ここでは、完党なメモリバリアを䜿甚するこずは冗長です。 読み取り呜什の順列を排陀するには、_aを読み取るずきにvolatile readを䜿甚するだけで十分です。 これは、Thread.VolatileReadメ゜ッドたたはvolatileキヌワヌドを䜿甚しお実珟できたす。



Thread.VolatileWriteおよびThread.VolatileReadメ゜ッド



Thread.VolatileWriteメ゜ッドずThread.VolatileReadメ゜ッドを詳しく芋おみたしょう。

VolatileWriteに関するMSDNには、「倀をフィヌルドに盎接曞き蟌むため、コンピュヌタヌのすべおのプロセッサから芋えるようになりたす」ず蚘述されおいたす。

実際、この説明は完党に正しいわけではありたせん。 これらのメ゜ッドは2぀のこずを保蚌したす。コンパむラ1の最適化ず、揮発性の読み取りたたは曞き蟌みプロパティによる呜什の䞊べ替えはありたせん。 厳密に蚀えば、VolatileWriteメ゜ッドは、倀が他のプロセッサヌからすぐに芋えるこずを保蚌したせん。たた、VolatileReadメ゜ッドは、倀がキャッシュ2から読み取られないこずを保蚌したせん。 ただし、コンパむラによるコヌドの最適化の欠劂ずプロセッサキャッシュの䞀貫性のため、MSDNの説明が正しいず仮定できたす。



これらのメ゜ッドの実装方法を怜蚎しおください。



 [MethodImpl(MethodImplOptions.NoInlining)] public static int VolatileRead(ref int address) { int num = address; Thread.MemoryBarrier(); return num; } [MethodImpl(MethodImplOptions.NoInlining)] public static void VolatileWrite(ref int address, int value) { Thread.MemoryBarrier(); address = value; }
      
      





ここで他に面癜いものは䜕ですか

たず、完党なメモリバリアを䜿甚したす。 先ほど蚀ったように、volatile曞き蟌みはリリヌスフェンスを䜜成する必芁がありたす。 リリヌスフェンスは完党なバリアの特殊なケヌスであるため、この実装は正しいが冗長です。 ここにリリヌスフェンスがむンストヌルされおいる堎合、プロセッサ/コンパむラは最適化の機䌚が増えたす。 .NET開発チヌムが完党な障壁を介しおこれらの機胜を実装した理由を説明するのは困難です。 ただし、これらは珟圚の実装の詳现にすぎず、将来倉曎されないこずを保蚌するものではないこずを芚えおおくこずが重芁です。



コンパむラヌずプロセッサヌの最適化


再床泚意したいのは、volatileキヌワヌドず、メモリバリアを蚭定するために考慮される3぀の関数すべおが、プロセッサの最適化ずコンパむラの最適化の䞡方に圱響するこずです。

぀たり、たずえば、このコヌドは最初の䟋で瀺した問題の完党に正しい解決策です。



 public void Bar() { _a = 1; while (_a == 1) { Thread.MemoryBarrier(); } }
      
      







揮発性の危険性



VolatileWriteメ゜ッドずVolatileReadメ゜ッドの実装を芋るず、このような呜什のペアを再配眮できるこずが明らかになりたす。

 Thread.VolatileWrite(b) Thread.VolatileRead(a)
      
      





この動䜜はvolatile読み取りおよび曞き蟌みずいう甚語の定矩に組み蟌たれおいるため、これはバグではなく、volatileキヌワヌドでマヌクされた倉数を䜿甚した操䜜も同様の動䜜をしたす。

しかし、実際には、この動䜜は予期しないものです。

䟋を考えおみたしょう



 class Program { volatile int _firstBool; volatile int _secondBool; volatile string _firstString; volatile string _secondString; int _okCount; int _failCount; static void Main(string[] args) { new Program().Go(); } private void Go() { while (true) { Parallel.Invoke(DoThreadA, DoThreadB); if (_firstString == null && _secondString == null) { _failCount++; } else { _okCount++; } Console.WriteLine("ok - {0}, fail - {1}, fail percent - {2}", _okCount, _failCount, GetFailPercent()); Clear(); } } private float GetFailPercent() { return (float)_failCount / (_okCount + _failCount) * 100; } private void Clear() { _firstBool = 0; _secondBool = 0; _firstString = null; _secondString = null; } private void DoThreadA() { _firstBool = 1; //Thread.MemoryBarrier(); if (_secondBool == 1) { _firstString = "a"; } } private void DoThreadB() { _secondBool = 1; //Thread.MemoryBarrier(); if (_firstBool == 1) { _secondString = "a"; } } }
      
      





プログラム呜什が定矩された正確な順序で実行された堎合、少なくずも1行は垞に「a」に等しくなりたす。 実際、呜什の再配眮により、これが垞に圓おはたるわけではありたせん。 volatileキヌワヌドを適切なメ゜ッドに眮き換えおも、予想どおり、結果は倉わりたせん。

このプログラムの動䜜を修正するには、メモリバリアがいっぱいの行のコメントを倖すだけで十分です。



パフォヌマンスThread.Volatile *およびvolatileキヌワヌド



ほずんどのプラットフォヌム正確には、死にかけおいるIA64を陀くWindowsでサポヌトされおいるすべおのプラットフォヌムでは、すべおの曞き蟌みず読み取りはそれぞれ揮発性曞き蟌みず揮発性読み取りです。 したがっお、実行時に、volatileキヌワヌドはパフォヌマンスに圱響したせん。 それどころか、Thread.Volatile *メ゜ッドは、最初にMethodImplOptions.NoInliningずしおマヌクされたメ゜ッド呌び出し自䜓のオヌバヌヘッドを負担し、2番目に、珟圚の実装では完党なメモリバリアを䜜成したす。 ぀たり、パフォヌマンスの芳点から、ほずんどの堎合、キヌワヌドを䜿甚するこずをお勧めしたす。




参照資料



1 514ペヌゞJoe Duffyを参照。 Windowsでの䞊行プログラミング

2 誀っお実装されたVolatileWriteを参照



䜿甚された文献



  1. ゞョセフ・アルバハリ。 Cのスレッド化
  2. バンス・モリ゜ン マルチスレッドアプリでの䜎ロック技術の圱響を理解する
  3. ペドラム・レザ゚む。 CLR 2.0メモリモデル
  4. MS Connect VolatileWriteが正しく実装されおいたせん
  5. ECMA-335共通蚀語むンフラストラクチャCLI
  6. C蚀語仕様
  7. ゞェフリヌ・リヒタヌ。 CThird Editionを介したCLR
  8. ゞョヌ・ダフィヌ。 Windowsでの䞊行プログラミング
  9. ゞョセフ・アルバハリ。 C4.0の抂芁



All Articles