CたたはJohn Skeet察Jeffrey Richterのスレッドセヌフむベント



私は䜕ずかCに関するむンタビュヌの準備をしおいたしたが、ずりわけ次の内容に぀いお質問を芋぀けたした。
「倚数のスレッドが垞にむベントをサブスクラむブおよびサブスクラむブ解陀しおいる堎合、Cでスレッドセヌフなむベントコヌルを線成する方法は」


質問は非垞に具䜓的で明確に提起されおいるので、私はそれに察する答えも明確か぀明確に䞎えるこずができるこずを疑いさえしたせんでした。 しかし、私は非垞に間違っおいたした。 これは非垞に人気があり、ボロボロですが、ただ開かれたトピックであるこずが刀明したした。 ロシア語の資料ではこの問題にほずんど泚意が払われおいないためHabrも䟋倖ではありたせん、私はこの問題に関しお芋぀けお消化したすべおの情報を収集するこずにしたした。
たた、ゞョンスキヌトずゞェフリヌリヒタヌに぀いおも説明したす。実際、マルチスレッド環境でむベントがどのように機胜するかずいう問題をよく理解する䞊で、圌らは重芁な圹割を果たしたした。

特に泚意深い読者は、蚘事の䞭に2぀のxkcdスタむルのコミックを芋぀けるでしょう。
泚意、2぀の写真の䞭は玄300-400 kbです


回答が必芁な質問を耇補したす。

「倚数のスレッドが垞にむベントをサブスクラむブおよびサブスクラむブ解陀しおいる堎合、Cでスレッドセヌフなむベントコヌルを線成する方法は」


いく぀かの質問はCを介したCLRブックに基づいおいるずいう仮定がありたしたが、Nutshellでお気に入りのC5.0はそのような質問にたったく察応しおいなかったので、ゞェフリヌリヒタヌCを介したCLRから始めたしょう。

ゞェフリヌ・リヒタヌの道


曞面からの抜粋

長い間、むベントを呌び出すための掚奚される方法は、次の構成に぀いおでした。

オプション1
public event Action MyLittleEvent; ... protected virtual void OnMyLittleEvent() { if (MyLittleEvent != null) MyLittleEvent(); } 


このアプロヌチの問題は、 OnMyLittleEventメ゜ッドで1぀のスレッドがMyLittleEventむベントMyLittleEvent nullでMyLittleEventないこずを確認でき、このチェックの盎埌、ただしむベントがMyLittleEvent前に、他のスレッドがサブスクラむバヌのリストからデリゲヌトを削陀しお、 MyLittleEvent nullむベントMyLittleEvent nullであり、むベントコヌルポむントでNullReferenceExceptionをNullReferenceExceptionたす。

この状況を明確に瀺す小さなxkcdスタむルのコミックを次に瀺したす2぀のスレッドが䞊行しお動䜜し、䞊から䞋に時間が経過したす。
展開する



䞀般に、すべおが論理的であり、通垞の競合状態 以䞋、競合状態がありたす。 そしお、Richterがこの問題をどのように解決するかを以䞋に瀺したすそしお、このオプションが最もよく遭遇したす

むベントを呌び出すメ゜ッドにロヌカル倉数を远加し、「゚ントリ」の時点でむベントをメ゜ッドにコピヌしたす。 デリゲヌトは䞍倉オブゞェクト以降-䞍倉であるため、むベントの「凍結」コピヌを取埗したす。このコピヌからは誰も賌読を解陀できたせん。 むベントのサブスクMyLittleEvent解陀するず、新しいデリゲヌトオブゞェクトが䜜成され、 MyLittleEventフィヌルドのオブゞェクトが眮き換えられたすが、 叀いデリゲヌトオブゞェクトぞのロヌカル参照が残っおいたす。

オプション2
 protected virtual void OnMyLittleEvent() { Action tempAction = MyLittleEvent; // ""      //   tempAction     ,      null     if (tempAction != null) tempAction (); } 


Richterによれば、JITコンパむラヌは最適化のためにロヌカル倉数の䜜成を単に省略し、2番目のオプションから最初の倉数を䜜成する、぀たり「フリヌズ」むベントをスキップするこずができるず説明されおいたす。 その結果、 Volatile.Read(ref MyLittleEvent)介しおコピヌするこずをお勧めしたす。

オプション3
 protected virtual void OnMyLittleEvent() { //      Action tempAction = Volatile.Read(ref MyLittleEvent); if (tempAction != null) tempAction (); } 


Volatileに぀いおは長い間個別に語るこずができたすが、䞀般的な堎合は「䞍芁なJITコンパむラヌの最適化を取り陀くこずができたす」。 この䞻題に぀いおは、さらに明確化ず詳现がありたすが、ここでは、ゞェフリヌリヒタヌによる珟圚の決定の䞀般的な考え方に焊点を圓おたす。

スレッドセヌフなむベント呌び出しを確実にするには、むベントをロヌカル倉数にコピヌしお珟圚のサブスクラむバヌのリストを「フリヌズ」し、受信したリストが空でない堎合、「フリヌズ」リストからすべおのハンドラヌを呌び出したす。 したがっお、可胜性のあるNullReferenceException.を取り陀きNullReferenceException.


私はすぐに、サブスクラむブされおいないオブゞェクト/スレッドでむベントをトリガヌしおいるずいう事実に混乱したした。 誰かがそのようにサブスクラむブを解陀するこずはほずんどありたせん-トレヌスの䞀般的な「クリヌニング」䞭に誰かがこれを行った可胜性がありたす-曞き蟌み/読み取りストリヌムたずえば、むベントでファむルにデヌタを曞き蟌むロガヌを閉じお、接続を閉じたすなど、぀たり、ハンドラヌを呌び出した時点でのサブスクラむブするオブゞェクトの内郚状態は、以降の䜜業に適さない可胜性がありたす。
たずえば、サブスクラむバがIDisposableメ゜ッドを実装し、解攟された以降、砎棄されるオブゞェクトでメ゜ッドを呌び出そうずするずObjectDisposedExceptionをスロヌするずいう芏則に埓っおいるずしたす。 たた、 Disposeメ゜ッドのすべおのむベントのDisposeを解陀するこずにも同意したす。
次に、このようなシナリオを想像しおください。別のスレッドがそのサブスクラむバヌのリストを「凍結」した盎埌に、このオブゞェクトでDisposeメ゜ッドを呌び出したす。 スレッドは、サブスクラむブされおいないオブゞェクトのハンドラヌを正垞に呌び出したす。そのハンドラヌは、むベントを凊理しようずしたずきに、遅かれ早かれオブゞェクトがすでに解攟されたこずを認識し、 ObjectDisposedExceptionをスロヌしたす。 ほずんどの堎合、この䟋倖はハンドラヌ自䜓にはキャッチされたせん。「サブスクラむバヌがサブスクラむブ解陀されお解攟された堎合、ハンドラヌが呌び出されるこずはない」ず仮定するこずは非垞に論理的だからです。 アプリケヌションのクラッシュ、アンマネヌゞリ゜ヌスのリヌク、たたはObjectDisposedException最初にObjectDisposedException 呌び出されたずきに䟋倖をキャッチした堎合むベント呌び出しは䞭断されたすが、むベントは通垞の「ラむブ」ハンドラヌに到達したせん。

挫画本に戻る。 物語は同じです-2぀のストリヌム、時間がダりンしたす。 実際に起こるこずは次のずおりです。
展開する



私の意芋では、この状況は、むベントがNullReferenceExceptionずきに発生する可胜性のあるNullReferenceExceptionよりもはるかに深刻です。
興味深いこずに、監芖察象オブゞェクトの䞡偎でスレッドセヌフむベントトリガヌを実装するためのヒントがありたすが、スレッドセヌフハンドラヌを実装するためのヒントはありたせん。

StackOverflowは䜕に぀いお話しおいるのですか


SOでは、この問題に特化した詳现な「蚘事」を芋぀けるこずができたすはい、この質問は小さな蚘事党䜓を描いおいたす。

䞀般に、私の芋解はそこで共有されおいたすが、この同志が远加するものは次のずおりです。

ロヌカル倉数に関するこの誇倧広告はすべお、 Cargo Cult Programmingに他ならないように思えたす。 倚数の人々がこの方法でスレッドセヌフむベントの問題を解決したすが、 完党なスレッドセヌフを実珟するにはさらに倚くの䜜業が必芁です。 このようなチェックをコヌドに远加しない人は、コヌドなしでも実行できるず自信を持っお蚀えたす。 この問題は、シングルスレッド環境には存圚したせん。オンラむンコヌド䟋のvolatileキヌワヌドを満たすこずはめったにないこずを考えるず、この远加チェックは無意味かもしれたせん。 NullReferenceExceptionを远跡するこずが目暙の堎合、クラスオブゞェクトの初期化䞭に空のdelegate { }むベントに割り圓おるこずで、 nullをたったくチェックせずに実行できたすか


これにより、問題の別の解決策が埗られたす。

 public event Action MyLittleEvent = delegate {}; 


MyLittleEventがnullになるこずはありたせん。远加のチェックをMyLittleEventできたす。 マルチスレッド環境では、むベントサブスクラむバヌの远加ず削陀を同期するだけで枈みたすが、 NullReferenceExceptionを受け取るこずを恐れずに呌び出すこずができたす。

オプション4
 public event Action MyLittleEvent = delegate {}; protected virtual void OnMyLittleEvent() { // ,   MyLittleEvent(); } 


前のアプロヌチず比范したこのアプロヌチの唯䞀の欠点は、空のむベントを呌び出すための小さなオヌバヌヘッドですオヌバヌヘッドは呌び出しごずに玄5ナノ秒であるこずが刀明したした。 たた、異なるむベントを持぀倚数の異なるクラスの堎合、むベントのこれらの空の「ギャグ」は倚くのRAMを占有したすが、C3.0 からSOの回答で John Skeetを信じるず、コンパむラは同じものを䜿甚したす同じオブゞェクトは、すべおの「ギャグ」の空のデリゲヌトです。 結果のILコヌドをチェックするずき、このステヌトメントは確認されず、むベントごずに空のデリゲヌトが䜜成されるこずを自分で远加したすLINQPadずILSpyを䜿甚しおチェックしたす。 極端な堎合には、プログラムのすべおの郚分からアクセスできる空のデリゲヌトを䜿甚しお、プロゞェクトに共通の静的フィヌルドを䜜成できたす。

ゞョン・スキヌトの道



ゞョンスキヌトに着いたので、スレッドセヌフむベントの実装に泚目する䟡倀がありたす。これは、 デリゲヌトおよびむベントセクションのCの詳现  オンラむン蚘事および同志Klotos による翻蚳 で説明されおいたす。

䞀番䞋の行は、 lock add 、 remove 、およびlocal "freeze"を閉じるこずです。これにより、耇数のスレッドのむベントを同時にサブスクラむブしながら、起こりうる䞍確実性を取り陀くこずができたす。

いく぀かのコヌド
 SomeEventHandler someEvent; readonly object someEventLock = new object(); public event SomeEventHandler SomeEvent { add { lock (someEventLock) { someEvent += value; } } remove { lock (someEventLock) { someEvent -= value; } } } protected virtual void OnSomeEvent(EventArgs e) { SomeEventHandler handler; lock (someEventLock) { handler = someEvent; } if (handler != null) { handler (this, e); } } 



このメ゜ッドは非掚奚であるずいう事実にもかかわらずC4.0以降のむベントの内郚実装はたったく異なるようです。蚘事の最埌にある゜ヌスのリストを参照しおください、 lockでむベントコヌル、サブスクリプション、およびサブスクラむブをラップできないこずを明確に瀺しおいたす。これは、デッドロック以䞋、デッドロックに぀ながる可胜性が非垞に高いです。 ロヌカル倉数ぞのコピヌのみがlockあり、むベント自䜓はこの構造の倖郚で呌び出されたす。

しかし、これは未登録むベントのハンドラヌを呌び出す問題を完党に解決するわけではありたせん。

SOの質問に戻りたす。 ダニ゚ルは、 NullReferenceExceptionを防ぐためのすべおの方法に察応しお、非垞に興味深い考えを持っおいたす。

はい、すべおのコストでNullReferenceExceptionを防止しようずするこずに぀いお、このNullReferenceExceptionを本圓に理解したした。 私たちの特定のケヌスでは、別のスレッドがむベントのサブスクラむブを解陀しおいる堎合にのみNullReferenceExceptionが発生する可胜性があるこずを話しおいたす。 そしお圌は、 むベントを二床ず受け取らないようにするためだけにこれを行いたす。実際、ロヌカル倉数のチェックを䜿甚する堎合、これは達成されたせん 。 レヌスの状態を非衚瀺にする堎合、それを開いお結果を修正できたす 。 NullReferenceException䜿甚NullReferenceExceptionず、むベントの䞍適切な凊理の瞬間を刀断できNullReferenceException 。 䞀般に、このコピヌずチェックの手法は、コヌドに混乱ずノむズを远加する単玔なカヌゎカルトプログラミングであるず䞻匵したすが、マルチスレッドむベントの問題はたったく解決したせん。


ずりわけ、ゞョン・スキヌトは質問に答えたした、そしお、これは圌が曞いおいるものです。

ゞョン・スキヌトvs.ゞェフリヌ・リヒタヌ



JITコンパむラヌには、条件があるため、デリゲヌトぞのロヌカル参照を最適化する暩利がありたせん。 この情報はしばらく前に「投げられた」が、これは真実ではないJoe DuffyたたはVance Morrisonのいずれかでこの質問を明確にした。 volatileないず、デリゲヌトぞのロヌカル参照が少し叀くなる可胜volatileだけです。 これにより、 NullReferenceExceptionは発生したせん。

そしお、はい、間違いなく競合状態にありたす、あなたは正しいです。 しかし、垞に存圚したす。 nullチェックを削陀しお、次のように曞くずしたしょう。
 MyLittleEvent(); 

サブスクラむバヌのリストが1000人のデリゲヌトで構成されおいるず想像しおください。 サブスクラむバヌの1人がむベントのサブスクリプションを解陀する前に、むベントのトリガヌを開始する可胜性がありたす。 この堎合、叀いリストに残るため、呌び出されたすデリゲヌトが䞍倉であるこずを忘れないでください。 私の知る限り、これは完党に避けられたせん。
空のdelegate {};を䜿甚するdelegate {}; nullのむベントをチェックする必芁がnullが、これはレヌスの次の状態から私たちを救いたせん。 さらに、このメ゜ッドは、むベントの最新バヌゞョンを䜿甚するこずを保蚌したせん。


さお、この答えは2009幎に曞かれ、CLRはC4th editionを介しお-2012幎に曞かれたこずに泚意する必芁がありたす。
実際、 Volatile.Readを介しおロヌカル倉数にコピヌする堎合を説明する理由を理解しおいたせんでした。圌はSkeetの蚀葉をさらに確認しおいるからです。
JITコンパむラヌはロヌカルのtempAction倉数を最適化するこずで誀っお䜕ができるかを知っおいるため、 Volatile.Readを䜿甚したバヌゞョンを䜿甚するこずをお勧めしたすが、 オプション2は省略できたす。 理論的には 、これは将来倉曎される可胜性があるため、 オプション3を䜿甚するこずをお勧めしたす。 しかし、実際には、 Microsoftがこのような倉曎を行うこずはほずんどありたせん。これは、既補の膚倧な数のプログラムを砎壊する可胜性があるからです。


すべおが完党に混乱したす-䞡方のオプションは同等ですが、 Volatile.ReadオプションVolatile.Readより同等です。 たた、サブスクラむブされおいないハンドラヌを呌び出すずきに、競合状態からあなたを救うオプションはありたせん。

むベントを呌び出すスレッドセヌフな方法はたったく存圚しないのでしょうか なぜNullReferenceExceptionそうもないNullReferenceExceptionを防ぐのにそれほど倚くの時間ず劎力がNullReferenceException 、サブスクラむブされおいないハンドラヌの同等の可胜性のある呌び出しを防ぐのになぜですか これは分かりたせんでした。 しかし、答えを探す過皋で、私は他の倚くのこずに気づきたした。ここに小さな芁玄がありたす。

最埌に䜕がありたすか





技術的には、提瀺されたオプションはいずれもスレッドではありたせん-むベントをトリガヌする 安党な方法です。 さらに、デリゲヌトのロヌカルコピヌを䜿甚しおデリゲヌト怜蚌メ゜ッドを远加するず、誀ったセキュリティ感が生じたす 。 自分自身を完党に保護する唯䞀の方法は、特定のむベントのサブスクリプションが既に解陀されおいるかどうかをむベントハンドラヌに匷制的に確認させるこずです。 残念ながら、むベントを発生さNullReferenceExceptionずきにNullReferenceExceptionを防止する䞀般的な方法ずは異なり、ハンドラヌの芏定はありたせん。 別個のラむブラリを䜜成する堎合、ほずんどの堎合、ナヌザヌに䜕らかの方法で圱響を䞎えるこずはできたせん。むベントからサブスクラむブを解陀した埌、ハンドラヌが呌び出されないずクラむアントに匷制させるこずはできたせん。

これらすべおの問題を認識した埌、私はただCで​​のデリゲヌトの内郚実装に぀いお耇雑な気持ちを抱いおいたした。 䞀方で、これらは䞍倉であるため、 foreachお倉曎コレクションを列挙する堎合のようにInvalidOperationExceptionの可胜性はありたせんが、䞀方で、呌び出し䞭に誰かがむベントからサブスクラむブを解陀したかどうかを確認する方法はありたせん。 むベントホルダヌが実行できる唯䞀のこずは、 NullReferenceExceptionに察しお自身を保護し、サブスクラむバヌが䜕も台無しにしないようにするこずです。 その結果、提起された質問は次のように回答できたす。

サブスクラむブされおいないサブスクラむバヌのハンドラヌを呌び出す可胜性が垞にあるため、マルチスレッド環境でスレッドセヌフむベント呌び出しを提䟛するこずは䞍可胜です。 この䞍確実性は、「スレッドセヌフティ」ずいう甚語の定矩、特に条項ず矛盟しおいたす
実装は、耇数のスレッドから同時にアクセスされたずきに競合状態がないこずが保蚌されおいたす。



远加の読曞


もちろん、芋぀けたものをすべおコピヌ/翻蚳するこずはできたせんでした。 したがっお、盎接たたは間接的に䜿甚された゜ヌスのリストを残したす。

Source: https://habr.com/ru/post/J240385/


All Articles