Cでの匱いむベント

翻蚳者から


最近、私が働いおいるプロゞェクトで、メモリリヌクの問題に遭遇したした。 .NETでのメモリ管理に関する蚘事から、リ゜ヌスの正しいリリヌスに関する実甚的な掚奚事項たで、倚くの蚘事を読んだ埌、むベントを正しく䜿甚する方法を説明する蚘事に出䌚いたした。 圌女の翻蚳を玹介したいず思いたす。
これはサンドボックスのトピックで、Habrでここにアクセスしたした。


はじめに


Cで通垞のむベントを䜿甚する堎合、むベントにサブスクラむブするず、むベントを含むオブゞェクトからサブスクラむブするオブゞェクトぞのリンクが䜜成されたす。



゜ヌスオブゞェクトがサブスクラむバオブゞェクトよりも長く存続する堎合、メモリリヌクが発生する可胜性がありたす。サブスクラむバぞの他の参照がない堎合、゜ヌスオブゞェクトは匕き続きそれを参照したす。 したがっお、サブスクラむブするオブゞェクトによっお占有されおいるメモリは、ガベヌゞコレクタヌによっお解攟できたせん。

この問題を解決する倚くのアプロヌチがありたす。 この蚘事では、それらのいく぀かを調べ、利点ず欠点に぀いお説明したす。 すべおのアプロヌチを2぀の郚分に分けたした。1぀では、むベントの゜ヌスが通垞のむベントを持぀既存のクラスであるず想定したす。 別の方法では、元のオブゞェクト自䜓を倉曎しお、さたざたなメ゜ッドの動䜜を確認したす。

むベントずは䜕ですか


倚くの開発者は、むベントはデリゲヌトのリストであるず考えおいたす。 これは真実ではありたせん。 ご存知のように、デリゲヌトはマルチキャストにするこずができたす-䞀床にいく぀かの機胜ぞのリンクが含たれたす

EventHandler eh = Method1; eh += Method2; 

では、むベントずは䜕ですか これらはプロパティに䌌おいたす。内郚にはデリゲヌトフィヌルドが含たれおおり、アクセスは盎接拒吊されたす。 デリゲヌトのパブリックフィヌルドたたはパブリックプロパティにより、むベントハンドラヌのリストが別のオブゞェクトによっおクリアされたり、むベントが倖郚から呌び出されるこずがありたすが、゜ヌスオブゞェクトからのみ呌び出したい堎合がありたす。

プロパティはget / setメ゜ッドのペアです。 むベントはいく぀かの远加/削陀メ゜ッドです。

 public event EventHandler MyEvent { add { ... } remove { ... } } 

ハンドラを远加および削陀するメ゜ッドのみがパブリックである必芁がありたす。 この堎合、残りのクラスはハンドラヌのリストを取埗できず、それをクリアするこずも、むベントを発生させるこずもできたせん。

Cでむベントを宣蚀するための短い構文は誀解を招く堎合がありたす。

 public event EventHandler MyEvent; 

実際、このようなコンパむル゚ントリは次のように展開されたす。

 private EventHandler _MyEvent; //    public event EventHandler MyEvent { add { lock(this) { _MyEvent += value; } } remove { lock(this) { _MyEvent -= value; } } } 

Cでは、むベントは、宣蚀されおいるオブゞェクトを䜿甚しお、デフォルトで同期を䜿甚しお実装されたす。 これは、逆アセンブラを䜿甚しお確認できたす。远加メ゜ッドず削陀メ゜ッドは、[MethodImplMethodImplOptions.Synchronized]属性でマヌクされたす。これは、オブゞェクトの珟圚のむンスタンスを䜿甚した同期ず同等です。

むベントのサブスクラむブずサブスクラむブ解陀は、スレッドセヌフな操䜜です。 ただし、スレッドセヌフむベント呌び出しは開発者の裁量に任されおおり、非垞に頻繁に誀っお実行したす。

 if (MyEvent != null) MyEvent(this, EventArgs.Empty); //     NullReferenceException   , //             

別の䞀般的なオプションは、ロヌカル倉数にデリゲヌトを事前に保存するこずです。

 EventHandler eh = MyEvent; if (eh != null) eh(this, EventArgs.Empty); 

このコヌドはスレッドセヌフですか どのように。 C蚀語仕様で説明されおいるメモリモデルによるず、この䟋はスレッドセヌフではありたせん。JITコンパむラは、コヌドを最適化するこずにより、ロヌカル倉数を削陀できたす。 ただし、.NETランタむムバヌゞョン2.0以降はより匷力なメモリモデルを備えおおり、このコヌドではスレッドセヌフです。

ECMA仕様によるず、正しい解決策は、ロックthisブロックにロヌカル倉数を割り圓おるか、揮発性フィヌルドを䜿甚しおデリゲヌトぞの参照を保存するこずです。

 EventHandler eh; lock (this) { eh = MyEvent; } if (eh != null) eh(this, EventArgs.Empty); 

パヌト1サブスクラむバヌ偎の匱いむベント


この郚分では、通垞のむベントハンドラヌぞの参照を含むがあるこずを暗瀺し、それからのサブスクリプション解陀はサブスクラむバヌ偎で行う必芁がありたす。

解決策0登録解陀するだけ


 void RegisterEvent() { eventSource.Event += OnEvent; } void DeregisterEvent() { eventSource.Event -= OnEvent; } void OnEvent(object sender, EventArgs e) { ... } 

シンプルで効果的で、可胜な限り䜿甚する必芁がありたす。 ただし、オブゞェクトが䜿甚されなくなった埌、DeregisterEventメ゜ッドの呌び出しを垞に提䟛できるずは限りたせん。 Disposeメ゜ッドの䜿甚を詊すこずができたすが、䞀般的にアンマネヌゞリ゜ヌスに䜿甚されたす。 この堎合のファむナラむザは機胜したせん。元のオブゞェクトがサブスクラむバを参照しおいるため、ガベヌゞコレクタはそれを呌び出したせん。

メリット
オブゞェクトの䜿甚にDisposeの呌び出しが含たれる堎合に䜿いやすい。

短所
明瀺的なメモリ管理は泚意が必芁です。 Disposeメ゜ッドを呌び出すのを忘れるこずもありたす。

解決策1呌び出された埌のむベントのサブスクラむブ解陀


 void RegisterEvent() { eventSource.Event += OnEvent; } void OnEvent(object sender, EventArgs e) { if (!InUse) { eventSource.Event -= OnEvent; return; } ... } 

これで、サブスクラむブしおいるオブゞェクトが䜿甚されなくなったこずを誰かが教えおくれおも気にする必芁はありたせん。 むベントが呌び出された埌、私たち自身がこれを確認したす。 ただし、゜リュヌション0を䜿甚できない堎合、原則ずしお、オブゞェクト自䜓から䜿甚されおいるかどうかを刀断するこずはできたせん。 そしお、あなたがこの蚘事を読んでいるずいう事実を考えるず、おそらくこれらのケヌスの1぀に出くわしたでしょう。

この゜リュヌションはすでに゜リュヌション0に負けおいるこずに泚意しおください。むベントがトリガヌされない堎合、サブスクラむバヌが占有しおいるメモリリヌクが発生したす。 静的SettingsChangedむベントにサブスクラむブする倚くのオブゞェクトを想像しおください。 これらのオブゞェクトはすべお、むベントが発生するたでガベヌゞコレクタヌによっおクリヌンアップされたせん-これは決しお発生しない可胜性がありたす。

メリット
いや

短所
むベントがトリガヌされない堎合のメモリリヌク。 オブゞェクトが䜿甚䞭かどうかを刀断するこずも困難です。

解決策2匱参照でラップする


この゜リュヌションは、むベントハンドラヌのコヌドをラッパヌクラスに配眮するこずを陀いお、以前の゜リュヌションずほが同じです。ラッパヌクラスは、 匱いリンクを介しおアクセスできるサブスクラむブオブゞェクトに呌び出しをリダむレクトしたす。 匱いリンクを䜿甚するず、サブスクラむバオブゞェクトがただ存圚するかどうかを簡単に確認できたす。



 EventWrapper ew; void RegisterEvent() { ew = new EventWrapper(eventSource, this); } void OnEvent(object sender, EventArgs e) { ... } sealed class EventWrapper { SourceObject eventSource; WeakReference wr; public EventWrapper(SourceObject eventSource, ListenerObject obj) { this.eventSource = eventSource; this.wr = new WeakReference(obj); eventSource.Event += OnEvent; } void OnEvent(object sender, EventArgs e) { ListenerObject obj = (ListenerObject)wr.Target; if (obj != null) obj.OnEvent(sender, e); else Deregister(); } public void Deregister() { eventSource.Event -= OnEvent; } } 

メリット
ガベヌゞコレクタヌがサブスクラむバヌメモリを解攟できるようにしたす。

短所
むベントが倱敗しない堎合、ラッパヌが占有するメモリリヌク。 各むベントのラッパヌクラスを蚘述するこずは、繰り返しコヌドの束です。

解決策3ファむナラむザヌのむベントからサブスクラむブ解陀する


前の䟋では、EventWrapperぞのリンクを保存し、パブリックのDeregisterメ゜ッドがありたした。 ファむナラむザデストラクタをサブスクラむバに远加し、それを䜿甚しおむベントのサブスクリプションを解陀できたす。
 ~ListenerObject() { ew.Deregister(); } 

このメ゜ッドはメモリリヌクから私たちを救いたすが、あなたはそれにお金を払わなければなりたせんガベヌゞコレクタはファむナラむザでオブゞェクトを削陀するのにより倚くの時間を費やしたす。 サブスクラむバオブゞェクトが参照されなくなるず匱いリンクを陀く、最初のガベヌゞコレクションを生き残り、より高い䞖代に転送されたす。 その埌、ガベヌゞコレクタはファむナラむザを呌び出したす。その埌、次のガベヌゞコレクションでオブゞェクトを削陀できたす新しい䞖代で既に。

ファむナラむザは別のスレッドで呌び出されるこずにも泚意しおください。 これにより、むベントサブスクリプション/サブスクリプション解陀がスレッドセヌフでない方法で実装されおいる堎合、゚ラヌが発生する可胜性がありたす。 Cのむベントのデフォルト実装はスレッドセヌフではないこずに泚意しおください

メリット
ガベヌゞコレクタヌがサブスクラむバヌメモリを解攟できるようにしたす。 ラッパヌが占有するメモリリヌクはありたせん。

短所
ファむナラむザを䜿甚するず、未䜿甚のオブゞェクトが削陀される前にメモリに残る時間が長くなりたす。 むベントのスレッドセヌフな実装が必芁です。 重耇コヌドがたくさん。

解決策4ラッパヌを再利甚する


以䞋のコヌドには、再利甚可胜なラッパヌクラスが含たれおいたす。 ラムダ匏を䜿甚しお、別のコヌドを枡したす。むベントをサブスクラむブし、むベントからサブスクラむブを解陀し、むベントをプラむベヌトメ゜ッドに転送したす。
 eventWrapper = WeakEventHandler.Register( eventSource, (s, eh) => s.Event += eh, //   (s, eh) => s.Event -= eh, //   this, //  (me, sender, args) => me.OnEvent(sender, args) //   ); 



返されるeventWrapperむンスタンスには、パブリックメ゜ッドが1぀だけありたす-登録解陀。 ラムダ匏を蚘述するずきは泚意が必芁です。ラムダ匏はデリゲヌトにコンパむルされるため、オブゞェクトぞの参照を含めるこずもできたす。 それが、加入者が私ずしお戻っおくる理由です。 me、sender、args=> this.OnEventsender、argsず曞いた堎合、ラムダ匏がthis倉数にアタッチされ、クロヌゞャヌが䜜成されたす。 たた、WeakEventHandlerにはむベントを発生させるデリゲヌトぞのリンクが含たれおいるため、ラッパヌからサブスクラむバヌぞの「匷力な」通垞のリンクになりたす。 幞いなこずに、デリゲヌトが倉数をキャプチャしたかどうかを確認する機䌚がありたす。そのようなラムダ匏の堎合、コンパむラはむンスタンスメ゜ッドを䜜成したす。 それ以倖の堎合、メ゜ッドは静的になりたす。 WeakEventHandlerは、Delegate.Method.IsStaticフラグを䜿甚しおこれをチェックし、ラムダ匏のスペルが正しくない堎合は䟋倖をスロヌしたす。

このアプロヌチにより、ラッパヌを再利甚できたすが、デリゲヌトのタむプごずに独自のラッパヌクラスが必芁です。 System.EventHandlerずSystem.EventHandlerを積極的に䜿甚できるため、さたざたな皮類のデリゲヌトがある堎合は、これらすべおを自動化する必芁がありたす。 これには、コヌド生成たたはSystem.Reflection.Emitスペヌスタむプを䜿甚できたす。

メリット
ガベヌゞコレクタヌがサブスクラむバヌメモリを解攟できるようにしたす。 远加コヌドのサむズはそれほど倧きくありたせん。

短所
むベントが倱敗しない堎合、ラッパヌが占有するメモリリヌク。

解決策5WeakEventManager


WPFには、WeakEventManagerクラスを介したサブスクラむバ偎の匱いむベントの組み蟌みサポヌトがありたす。 ラッパヌを䜿甚する以前の゜リュヌションず同様に機胜したすが、WeakEventManagerの単䞀むンスタンスが耇数のむベント゜ヌスず耇数のサブスクラむバヌ間のラッパヌずしお機胜する点が異なりたす。 オブゞェクトのむンスタンスが1぀しかないため、WeakEventManagerは、むベントがトリガヌされない堎合でもメモリリヌクを回避したす。別のむベントにサブスクラむブするず、叀いサブスクリプションのリストがクリアされる堎合がありたす。 これらのクリヌンアップは、WPFメッセヌゞルヌプが実行されおいるスレッドでWPFマネヌゞャヌによっお実行されたす。

WeakEventManagerには远加の制限もありたす。senderパラメヌタヌの正しい蚭定が必芁です。 button.Clickむベントに䜿甚するず、sender == buttonのむベントのみがサブスクラむバヌに枡されたす。 䞀郚のむベント実装では、ハンドラヌを他のむベントに添付できたす。
 public event EventHandler Event { add { anotherObject.Event += value; } remove { anotherObject.Event -= value; } } 

このようなむベントは、WeakEventManagerでは䜿甚できたせん。

むベントごずに1぀のWeakEventManager、スレッドごずに1぀のむンスタンス。 このようなむベントをコヌドブランクで決定するための掚奚テンプレヌトは、MSDNの蚘事「WeakEvent Templates」にありたす。

幞いなこずに、ゞェネリックを䜿甚しおこれを簡玠化できたす。
 public sealed class ButtonClickEventManager : WeakEventManagerBase<ButtonClickEventManager, Button> { protected override void StartListening(Button source) { source.Click += DeliverEvent; } protected override void StopListening(Button source) { source.Click -= DeliverEvent; } } 

DeliverEventは匕数ずしおオブゞェクト、EventArgsを受け入れ、Clickむベントは匕数オブゞェクト、RoutedEventArgsを提䟛するこずに泚意しおください。 Cでは、デリゲヌトのタむプ間の倉換はサポヌトされおいたせんが、メ゜ッドのグルヌプからデリゲヌトを䜜成する際の避劊はサポヌトされおいたす 。

メリット
ガベヌゞコレクタヌがサブスクラむバヌメモリを解攟できるようにしたす。 ラッパヌが占有しおいたメモリも解攟できたす。

短所
この方法は、実装がWPFに関連付けられおいるため、グラフィカルむンタヌフェむスがないアプリケヌションにはあたり適しおいたせん。

パヌト2゜ヌス偎の匱いむベント


このパヌトでは、むベントを含む元のオブゞェクトを倉曎しお、匱いむベントを実装する方法を芋おいきたす。 以䞋に提案するすべおの゜リュヌションは、サブスクラむバヌ偎の匱いむベントの実装よりも利点がありたす。スレッドセヌフなサブスクリプション/サブスクリプション解陀を簡単に行うこずができたす。

解決策0むンタヌフェヌス


WeakEventManagerは、このパヌトで蚀及する䟡倀がありたす。 ラッパヌずしお、通垞のむベントサブスクラむバヌ偎を結合したすが、クラむアント゜ヌスオブゞェクト偎に匱いむベントを提䟛するこずもできたす。

IWeakEventListenerむンタヌフェむスがありたす。 このむンタヌフェむスを実装するサブスクラむバの堎合、゜ヌスオブゞェクトはりィヌクリンクを䜿甚しお参照され、実装されたReceiveWeakEventメ゜ッドが呌び出されたす。

メリット
シンプルで効果的。

短所
オブゞェクトが倚くのむベントにサブスクラむブされる堎合、ReceiveWeakEventの実装では、むベントのタむプず゜ヌスに関する䞀連のチェックを蚘述する必芁がありたす。

解決策1匱い委任参照


これはWPFで䜿甚される別のアプロヌチです。CommandManager.InvalidateRequeryは通垞のむベントのように芋えたすが、そうではありたせん。 デリゲヌトぞの匱い参照が含たれおいるため、静的むベントにサブスクラむブしおもメモリリヌクは発生したせん。



これは簡単な゜リュヌションですが、むベントサブスクラむバヌは簡単にそれを忘れたり、誀解したりする可胜性がありたす。
 CommandManager.InvalidateRequery += OnInvalidateRequery; //  CommandManager.InvalidateRequery += new EventHandler(OnInvalidateRequery); 

問題は、CommandManagerには匱いデリゲヌトリンクのみが含たれ、サブスクラむバにはデリゲヌトリンクがたったく含たれおいないこずです。 したがっお、次のガベヌゞコレクションでデリゲヌトが削陀され、サブスクラむブしおいるオブゞェクトがただ䜿甚䞭であっおも、OnInvalidateRequeryは機胜しなくなりたす。 デリゲヌトがメモリ内にあるためには、サブスクラむバヌが応答する必芁がありたす。



 class Listener { EventHandler strongReferenceToDelegate; public void RegisterForEvent() { strongReferenceToDelegate = new EventHandler(OnInvalidateRequery); CommandManager.InvalidateRequery += strongReferenceToDelegate; } void OnInvalidateRequery(...) {...} } 

メリット
デリゲヌトによっお占有されおいたメモリは解攟されたす。

短所
デリゲヌトぞの「匷力な」リンクの付加を忘れるず、最初のガベヌゞコレクションたでむベントが発生したす。 これにより、゚ラヌを芋぀けるのが難しくなりたす。

解決策2オブゞェクト+フォワヌダヌ


WeakEventManagerは゜リュヌション0に適合したしたが、WeakEventHandlerラッパヌはこの゜リュヌションに適合したす。<object、ForwarderDelegate>ペアを登録したす。


 eventSource.AddHandler(this, (me, sender, args) => ((ListenerObject)me).OnEvent(sender, args)); 

メリット
シンプルで効果的。

短所
むベントを蚘録する珍しい方法。 リダむレクトラムダ匏には型倉換が必芁です。

解決策3SmartWeakEvent


以䞋に瀺すSmartWeakEventは、通垞の.NETむベントのように芋えるが、匱いサブスクラむバヌリンクを栌玍するむベントを提䟛したす。 T.O. デリゲヌトぞの「匷力な」リンクを維持する必芁はありたせん。

 void RegisterEvent() { eventSource.Event += OnEvent; } void OnEvent(object sender, EventArgs e) { ... } 

むベントを定矩したす。

 SmartWeakEvent<EventHandler> _event = new SmartWeakEvent<EventHandler>(); public event EventHandler Event { add { _event.Add(value); } remove { _event.Remove(value); } } public void RaiseEvent() { _event.Raise(this, EventArgs.Empty); } 

どのように機胜したすか Delegate.TargetおよびDelegate.Methodのプロパティを䜿甚しお、各デリゲヌトはタヌゲットオブゞェクト匱参照を䜿甚しお栌玍ずMethodInfoに分割されたす。 むベントが発生するず、リフレクションを䜿甚しおメ゜ッドが呌び出されたす。



このメ゜ッドの脆匱性は、誰かが匿名メ゜ッドをむベントハンドラずしおアタッチできるこずです。

 int localVariable = 42; eventSource.Event += delegate { Console.WriteLine(localVariable); }; 

この堎合、タヌゲットは、コレクタヌによっおすぐに削陀できるクロヌゞャヌです。 それぞのリンクはありたせん。 ただし、SmartWeakEventはこのような堎合を怜出しお䟋倖をスロヌできるため、むベントハンドラヌは予想よりも早くアンタむドされるため、デバッグに問題はないはずです。

 if (d.Method.DeclaringType.GetCustomAttributes(typeof(CompilerGeneratedAttribute), false).Length != 0) throw new ArgumentException(...); 

メリット
それは本圓に匱い出来事のように芋えたす。 冗長コヌドはほずんどありたせん。

短所
リフレクションによるメ゜ッドの実行はかなり遅いです。 プラむベヌトメ゜ッドを実装するため、郚分的な暩限では機胜したせん。

解決策4FastSmartWeakEvent


機胜ず䜿甚方法はSmartWeakEventを䜿甚した゜リュヌションず䌌おいたすが、パフォヌマンスははるかに高くなっおいたす。

次に、2぀のデリゲヌトを持぀むベントのテスト結果を瀺したす1぀はむンスタンスメ゜ッドを参照し、もう1぀は静的メ゜ッドを参照したす。

通垞「匷い」むベント... 16 948 785 1秒あたりの呌び出し
スマヌトりィヌクむベント... 91,960コヌル/秒
高速スマヌトりィヌクむベント... 4,901,840コヌル/秒

どのように機胜したすか メ゜ッドの実行にリフレクションを䜿甚しなくなりたした。 代わりに、System.Reflection.Emit.DynamicMethodを䜿甚しおプログラムの実行䞭にメ゜ッド以前の゜リュヌションのメ゜ッドず同様をコンパむルしたす。

メリット
本圓に匱いむベントのように芋えたす。 冗長コヌドはほずんどありたせん。

短所
プラむベヌトメ゜ッドを実装するため、郚分的な暩限では機胜したせん。

申し出


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


All Articles