C#イベントとスレッドセーフ


237

更新

C#6以降この質問に対する答えは次のとおりです。

SomeEvent?.Invoke(this, e);

私は頻繁に次のアドバイスを聞いたり読んだりします:

確認して実行する前に、必ずイベントのコピーを作成してくださいnull。これにより、nullnullを確認する場所とイベントを発生させる場所の間にイベントが発生する、スレッドに関する潜在的な問題が解消されます。

// Copy the event delegate before checking/calling
EventHandler copy = TheEvent;

if (copy != null)
    copy(this, EventArgs.Empty); // Call any handlers on the copied list

更新:最適化について読んだことから、これにはイベントメンバーも揮発性である必要があるかもしれないと思いましたが、ジョンスキートは彼の回答でCLRはコピーを最適化しないと述べています。

しかし、その間、この問題が発生するためには、別のスレッドが次のようなことをしている必要があります。

// Better delist from event - don't want our handler called from now on:
otherObject.TheEvent -= OnTheEvent;
// Good, now we can be certain that OnTheEvent will not run...

実際のシーケンスは次のようになります。

// Copy the event delegate before checking/calling
EventHandler copy = TheEvent;

// Better delist from event - don't want our handler called from now on:
otherObject.TheEvent -= OnTheEvent;    
// Good, now we can be certain that OnTheEvent will not run...

if (copy != null)
    copy(this, EventArgs.Empty); // Call any handlers on the copied list

要点はOnTheEvent、作成者が購読を解除した後でありますが、それが発生しないように、特に購読を解除しただけです。確かに本当に必要なのは、addremoveアクセサで適切に同期するカスタムイベントの実装です。さらに、イベントが発生したときにロックが保持されると、デッドロックが発生する可能性があるという問題があります。

それでは、このカーゴカルトプログラミングですか?それはそのようです-マルチスレッド設計の一部として使用できるようになる前に、実際にはイベントはこれよりもはるかに注意を必要とするように思えますが、多くの人々はコードを複数のスレッドから保護するためにこのステップを踏まなければなりません。その結果、その追加の注意を払っていない人々はこのアドバイスを無視するかもしれません-それは単にシングルスレッドプログラムの問題ではありません、そして実際、volatileほとんどのオンラインサンプルコードに欠けているとすれば、アドバイスはないかもしれませんまったく効果。

(そして、最初delegate { }にチェックする必要がないように、メンバー宣言に空を割り当てるだけの方がずっと簡単ではないnullですか?)

更新しました:不明確な場合でも、私はアドバイスの意図を理解しました。あらゆる状況でnull参照例外を回避するためです。私のポイントは、この特定のnull参照例外は、別のスレッドがイベントから登録解除されている場合にのみ発生する可能性があることであり、これを行う唯一の理由は、そのイベントを介してそれ以上の呼び出しが受信されないようにすることです。これは、この手法では明らかに達成されません。 。あなたは競合状態を隠しているでしょう-それを明らかにする方が良いでしょう!このnull例外は、コンポーネントの不正使用を検出するのに役立ちます。コンポーネントを不正使用から保護したい場合は、WPFの例に従う-コンストラクターにスレッドIDを格納し、別のスレッドがコンポーネントと直接対話しようとした場合に例外をスローします。または、本当にスレッドセーフなコンポーネントを実装します(簡単な作業ではありません)。

したがって、このコピー/チェックのイディオムを実行するだけでは、カーゴカルトプログラミングであり、コードに混乱やノイズが加わると思います。他のスレッドから実際に保護するには、さらに多くの作業が必要です。

Eric Lippertのブログ投稿に応じて更新:

したがって、イベントハンドラーについて見逃していた重要なことがあります。「イベントハンドラーは、イベントがサブスクライブされた後でも呼び出される前に堅牢である必要がある」ため、イベントの可能性のみを考慮する必要があります。委任nullイベントハンドラーの要件はどこかに文書化されていますか?

そして、「この問題を解決する方法は他にもあります。たとえば、ハンドラーを初期化して、削除されない空のアクションを設定します。ただし、nullチェックを行うのが標準的なパターンです。」

だから私の質問の残りの断片は、なぜ「標準パターン」を明示的にヌルチェックするのですか?代わりに、空のデリゲートを割り当てる= delegate {}と、イベント宣言に追加するだけで済みます。これにより、イベントが発生するすべての場所から、これらの小さな臭い儀式の山がなくなります。空のデリゲートをインスタンス化するのが安価であることを確認するのは簡単です。または、まだ何か不足していますか?

確かにそれは(Jon Skeetが示唆したように)これは2005年に行われるはずだった、消えていない.NET 1.xのアドバイスにすぎないのでしょうか?


4
この質問は、しばらく前に社内で議論されました。私はこれをしばらくブログに書くつもりでした。この件に関する私の投稿はこちらです:イベントとレース
エリックリッペルト

3
Stephen Clearyは、この問題を検討するCodeProjectの記事書いており、「スレッドセーフ」なソリューションは存在しないという一般的な目的を結論付けています。基本的には、デリゲートがnullでないことを確認するのはイベントインボーカーであり、サブスクライブが解除された後に呼び出されることを処理できるのはイベントハンドラーです。
rkagerer、2011年

3
@rkagerer-実際には、スレッドが関与していなくても、2番目の問題はイベントハンドラーで処理する必要があります。あるイベントハンドラーが別のハンドラーに現在処理中のイベントをサブスクライブ解除するように指示した場合でも、その2番目のサブスクライバーがイベントを受信します(処理の進行中にサブスクライブ解除されたため)。
Daniel Earwicker

3
サブスクライバーがゼロのイベントへのサブスクリプションの追加、イベントの唯一のサブスクリプションの削除、サブスクライバーがゼロのイベントの呼び出し、サブスクライバーが1つだけのイベントの呼び出しは、他の数のシナリオを含む追加/削除/呼び出しシナリオよりもはるかに高速な操作です。加入者。ダミーのデリゲートを追加すると、一般的なケースが遅くなります。C#の本当の問題は、作成者EventName(arguments)がイベントのデリゲートを無条件に呼び出すのではなく、無条件にデリゲートを呼び出すことを決定したことです(nullの場合は何もしない)。
スーパーキャット

回答:


100

JITは、条件のため、最初の部分で説明している最適化を実行できません。これは少し前に妖怪として提起されたことは知っていますが、有効ではありません。(私は少し前にJoe DuffyかVance Morrisonのどちらかでそれをチェックしました;私はどちらを思い出せませんか。)

volatile修飾子がないと、取得されたローカルコピーが古くなる可能性がありますが、それだけです。は発生しませんNullReferenceException

もちろん、競合状態は確かにありますが、常に存在します。コードを次のように変更するとします。

TheEvent(this, EventArgs.Empty);

次に、そのデリゲートの呼び出しリストに1000のエントリがあるとします。別のスレッドがリストの終わり近くにあるハンドラーをサブスクライブ解除する前に、リストの最初のアクションが実行される可能性は完全にあり得ます。ただし、そのハンドラーは新しいリストになるため、引き続き実行されます。(デリゲートは不変です。)私が見る限り、これは避けられません。

空のデリゲートを使用すると、nullityチェックは確実に回避されますが、競合状態は修正されません。また、変数の最新の値が常に「表示」されることも保証されません。


4
Joe Duffyの「Windowsでの並行プログラミング」では、質問のJIT最適化とメモリモデルについて説明しています。code.logos.com/blog/2008/11/events_and_threads_part_4.htmlを
Bradley Grainger

2
「標準」のアドバイスがC#2より前であるというコメントに基づいてこれを受け入れましたが、それと矛盾する人は誰もいません。イベント引数をインスタンス化するのに本当にコストがかかる場合を除き、イベント宣言の最後に '= delegate {}'を配置して、メソッドのように直接イベントを呼び出します。それらにnullを割り当てないでください。(ハンドラーがリストから削除された後に呼び出されないようにするために私が持ち込んだ他のことは、無関係であり、シングルスレッドのコードであっても保証することは不可能です。次へ)
ダニエル・イアーウィッカー2009

2
(いつものように)唯一の問題は構造体であり、そのメンバーがnull値以外でインスタンス化されることを保証できません。しかし、構造体は吸う。
Daniel Earwicker、2009

1
空のデリゲートについては、次の質問も参照してください:stackoverflow.com/questions/170907/…
ウラジミール

2
@Tony:サブスクライブ/サブスクライブ解除と実行されるデリゲートの間には、根本的にまだ競合状態があります。コード(簡単に参照しただけ)は、発生中にサブスクリプション/サブスクリプション解除を有効にすることで競合状態を軽減しますが、通常の動作が十分でないほとんどの場合、これもどちらかではありません。
Jon Skeet、2014年

52

多くの人がこれを行う拡張方法に向かっているのを見ています...

public static class Extensions   
{   
  public static void Raise<T>(this EventHandler<T> handler, 
    object sender, T args) where T : EventArgs   
  {   
    if (handler != null) handler(sender, args);   
  }   
}

それはあなたにイベントを発生させるより良い構文を与えます...

MyEvent.Raise( this, new MyEventArgs() );

また、メソッドの呼び出し時にキャプチャされるため、ローカルコピーも不要です。


9
構文は好きですが、はっきりさせておきましょう...登録を解除しても、古いハンドラーが呼び出されるという問題は解決しません。これ、null逆参照問題のみを解決します。私は構文が好きですが、それが本当に次の点より優れているかどうかを質問します。public event EventHandler <T> MyEvent = delete {}; ... MyEvent(これ、新しいMyEventArgs()); これは、その単純さのために私が好きな非常に低摩擦のソリューションでもあります。
Simon Gillbee、2009年

@サイモン私はこれについて別の人が別のことを言っているのを見ます。私はそれをテストしました、そして私がやったことはこれがnullハンドラー問題を処理することを私に示します。ハンドラー!= nullチェックの後で元のシンクがイベントから登録解除されても、イベントは発生し、例外はスローされません。
JP Alioto

うん、この質問CF:stackoverflow.com/questions/192980/...
Benjol

1
+1。私はこの方法を自分で書き、スレッドセーフについて考え始め、いくつかの調査を行い、この質問に出くわしました。
Niels van der Rest

これをVB.NETから呼び出すにはどうすればよいですか?または、「RaiseEvent」はすでにマルチスレッドシナリオに対応していますか?

35

「なぜ「標準パターン」を明示的にヌルチェックするのですか?」

この理由は、nullチェックの方がパフォーマンスが高いためだと思います。

イベントの作成時に常に空のデリゲートをイベントにサブスクライブすると、いくつかのオーバーヘッドが発生します。

  • 空のデリゲートを構築するコスト。
  • それを含むデリゲートチェーンを構築するコスト。
  • イベントが発生するたびに無意味なデリゲートを呼び出すコスト。

(UIコントロールには多数のイベントが含まれることが多く、そのほとんどがサブスクライブされないことに注意してください。各イベントにダミーサブスクライバーを作成して呼び出す必要があると、パフォーマンスが大幅に低下する可能性があります。)

私はいくつかの大まかなパフォーマンステストを行って、subscribe-empty-delegateアプローチの影響を確認しました。これが私の結果です。

Executing 50000000 iterations . . .
OnNonThreadSafeEvent took:      432ms
OnClassicNullCheckedEvent took: 490ms
OnPreInitializedEvent took:     614ms <--
Subscribing an empty delegate to each event . . .
Executing 50000000 iterations . . .
OnNonThreadSafeEvent took:      674ms
OnClassicNullCheckedEvent took: 674ms
OnPreInitializedEvent took:     2041ms <--
Subscribing another empty delegate to each event . . .
Executing 50000000 iterations . . .
OnNonThreadSafeEvent took:      2011ms
OnClassicNullCheckedEvent took: 2061ms
OnPreInitializedEvent took:     2246ms <--
Done

サブスクライバーがゼロまたは1つの場合(イベントが豊富なUIコントロールでよく見られる)は、空のデリゲートで事前に初期化されたイベントは著しく遅い(5000万回を超える繰り返し)ことに注意してください。

詳細とソースコードについては、この質問をする前日に公開した.NETイベント呼び出しスレッドの安全性に関するこのブログ投稿をご覧ください(!)

(私のテスト設定には欠陥があるかもしれないので、自由にソースコードをダウンロードして自分で検査してください。どんなフィードバックでも大歓迎です。)


8
私はあなたがブログ投稿の重要なポイントを作ったと思います:それがボトルネックになるまで、パフォーマンスへの影響を心配する必要はありません。なぜ醜い方法を推奨する方法にしようか?明快さの代わりに時期尚早の最適化が必要な場合は、アセンブラを使用します-私の質問は残ります、そしておそらく答えは匿名のデリゲートよりも古いものであり、人間の文化が古いアドバイスをシフトするのに長い時間がかかるということです有名な「鍋焼き物語」で。
Daniel Earwicker、2009年

13
そして、あなたの数値は要点を非常によく示しています:オーバーヘッドは、発生したイベントごとにわずか2と半分のNANOSECONDS(!!!)に下がります(初期化前と従来のnullの場合)。これは、実際の作業を行うほとんどのアプリでは検出されませんが、イベントの使用の大部分がGUIフレームワークで行われているため、Winformsなどで画面の一部を再描画するコストと比較する必要があります。実際のCPU作業とリソースを待機している大洪水では、それはさらに見えなくなります。とにかく、あなたはハードワークのために私から+1をもらいます。:)
Daniel Earwicker、2009年

1
@DanielEarwickerはそれを正しく言った、あなたは私を公開イベントWrapperDoneHandler OnWrapperDone =(x、y)=> {};の信者になるように動かした。モデル。
ミッキーペルスタイン

1
イベントのサブスクライバーが0個、1個、または2個の場合は、Delegate.Combine/ Delegate.Removeペアの時間を計るのもよいでしょう。同じデリゲートインスタンスを繰り返し追加および削除するとCombine、一方の引数がnull(もう一方を返すだけ)である場合は高速の特殊なケースの動作をRemoveし、2つの引数が非常に速い場合は、ケース間のコストの違いが特に顕著になります等しい(nullを返すだけ)。
スーパーキャット2013年

12

私はこれを本当に楽しんだ-そうではない!イベントと呼ばれるC#機能を使用するために必要なのに!

コンパイラでこれを修正しないのはなぜですか?私はこれらの投稿を読んでいるMSの人々がいることを知っているので、これを非難しないでください!

1-Nullの問題)そもそもなぜイベントをnullの代わりに.Emptyにしないのですか?nullチェックのために何行のコードが保存されるか、または= delegate {}宣言に固執する必要がありますか?コンパイラーが空のケースを処理するようにします。IEは何もしません。イベントの作成者にとってそれがすべて重要である場合、彼らは.Emptyをチェックし、気になっていることを何でも行うことができます!それ以外の場合、すべてのnullチェック/デリゲートの追加は問題のハックです!

正直なところ、私はすべてのイベントでこれを行う必要があることにうんざりしています-別名定型コード!

public event Action<thisClass, string> Some;
protected virtual void DoSomeEvent(string someValue)
{
  var e = Some; // avoid race condition here! 
  if(null != e) // avoid null condition here! 
     e(this, someValue);
}

2-競合状態の問題)私はエリックのブログ投稿を読みましたが、H(ハンドラー)がそれ自体を逆参照するときに処理することに同意しますが、イベントを不変/スレッドセーフにすることはできませんか?IEは、作成時にロックフラグを設定します。これにより、呼び出されるたびに、実行中にすべてのサブスクライブとアンサブスクライブがロックされますか?

まとめ

私たちにとって、現代の言語はこのような問題を解決するはずではありませんか?


同意しました、これはコンパイラでより良いサポートがあるはずです。それまでは、コンパイル後の手順でこれを行うPostSharpアスペクト作成しました。:)
Steven Jeuris

4
完全に任意の外部のコードのために待っている間に、スレッドの購読/退会要求がブロックさ波打つはるかに悪い加入者は、サブスクリプションの後にイベントを受け取ることよりも、後者「問題」を解決することができ、特に以来、キャンセルされやすく、単純にイベントハンドラがどうかを確認するためのフラグをチェックしたことにより、彼らはまだイベントの受信に興味を持っていますが、前の設計に起因するデッドロックは扱いにくいかもしれません。
スーパーキャット2013年

@supercat。イモ、「はるかに悪い」コメントはかなりアプリケーションに依存しています。オプションの場合、追加のフラグなしで非常に厳密なロックを望まないのは誰ですか?ロックは同じスレッドの再入可能であり、元のイベントハンドラー内のサブスクライブ/サブスクライブがブロックされないため、イベント処理スレッドがさらに別のスレッド(サブスクライブ/サブスクライブ解除)で待機している場合にのみデッドロックが発生します。設計の一部となるイベントハンドラーの一部として待機しているクロススレッドがある場合は、やり直したいと思います。私は、予測可能なパターンを持つサーバー側のアプリアングルから来ています。
crokusek 2013年

2
@crokusek:各ロックを保持中に必要になる可能性のあるすべてのロックに接続する有向グラフにサイクルがない場合、システムにデッドロックがないことを証明するために必要な分析は簡単です[サイクルの欠如はシステムを証明しますデッドロックフリー]。ロックが保持されている間に任意のコードが呼び出されることを許可すると、「必要になる可能性がある」グラフに、そのロックから任意のコードが取得する可能性のある任意のロックへのエッジが作成されます(システム内のすべてのロックではなく、それほど遠くない) )。結果として生じるサイクルの存在は、デッドロックが発生することを意味するわけではありませんが、...
スーパーキャット2013年

1
...それができなかったことを証明するために必要な分析レベルを大幅に上げるでしょう。
スーパーキャット2013年

8

C#6と、上記のコードは、新しい使用簡略化することができた?.とオペレータが:

TheEvent?.Invoke(this, EventArgs.Empty);

こちらがMSDNのドキュメントです。


5

書籍CLR via CLRの Jeffrey Richterによると、正しい方法は次のとおりです。

// Copy a reference to the delegate field now into a temporary field for thread safety
EventHandler<EventArgs> temp =
Interlocked.CompareExchange(ref NewMail, null, null);
// If any methods registered interest with our event, notify them
if (temp != null) temp(this, e);

それは参照コピーを強制するからです。詳細については、本の彼のイベントセクションを参照してください。


最初の引数がnullである場合、Interlocked.CompareExchangeはNullReferenceExceptionをスローしますが、これはまさに回避したいものです。msdn.microsoft.com/en-us/library/bb297966.aspx
Kniganapolke

1
Interlocked.CompareExchangenull refが渡された場合に失敗しますが、これは、存在し、最初 null参照を保持refしている保管場所(などNewMail)に渡された場合と同じではありません。
スーパーキャット2013

4

このデザインパターンを使用して、サブスクライブ解除後にイベントハンドラーが実行されないようにしています。これまでのところ、かなりうまく機能していますが、パフォーマンスのプロファイリングは試していません。

private readonly object eventMutex = new object();

private event EventHandler _onEvent = null;

public event EventHandler OnEvent
{
  add
  {
    lock(eventMutex)
    {
      _onEvent += value;
    }
  }

  remove
  {
    lock(eventMutex)
    {
      _onEvent -= value;
    }
  }

}

private void HandleEvent(EventArgs args)
{
  lock(eventMutex)
  {
    if (_onEvent != null)
      _onEvent(args);
  }
}

最近は主にMono for Androidを使用していますが、アクティビティがバックグラウンドに送信された後でビューを更新しようとすると、Androidが気に入らないようです。


実は、私は他の誰かがここに非常に類似したパターンを使用している参照してください。stackoverflow.com/questions/3668953/...
アッシュ

2

これは、特定の順序の操作を強制することではありません。これは、実際にはnull参照例外を回避するためのものです。

競合状態ではなくnull参照例外を気にする人々の背後にある推論には、いくつかの深い心理学的研究が必要です。null参照の問題を修正する方がはるかに簡単であるという事実と関係があると思います。それが修正されると、彼らは大きな "Mission Accomplished"バナーをコードにぶら下げ、フライトスーツを解凍します。

注:競合状態の修正には、ハンドラーを実行するかどうかの同期フラグトラックの使用が含まれる可能性があります


私はその問題の解決策を求めているのではありません。まだ検出されない競合状態が存在する場合にnull例外を回避するだけで、イベントの発火に余分なコード混乱を散布する幅広いアドバイスがあるのはなぜだろうと思います。
Daniel Earwicker、2009

1
まあそれが私のポイントでした。彼らは競合状態を気にしません。それらは、null参照例外のみを考慮します。私はそれを私の答えに編集します。
dss539 2009

そして私のポイントは、null参照の例外に注意を払いながら、競合状態を気にしないのはなぜでしょうか。
Daniel Earwicker、2009

4
適切に作成されたイベントハンドラーは、イベントを発生させる特定の要求が処理を追加または削除する要求とオーバーラップする可能性があるという事実を処理するように準備する必要があります。プログラマーが競合状態を気にしない理由は、適切に記述されたコードでは誰が勝つかは関係ないからです
スーパーキャット

2
@ dss539:保留中のイベント呼び出しが終了するまでUnloadサブスクリプション解除要求をブロックするイベントフレームワークを設計できますが、そのような設計により、イベント(イベントのようなものでも)が他のイベントへのオブジェクトのサブスクリプションを安全にキャンセルできなくなります。不快な。より良いのは、イベントのサブスクリプション解除要求によってイベントが「最終的に」サブスクライブ解除され、イベントサブスクライバーが呼び出されたときに、何か役立つことがあるかどうかを確認する必要があるということです。
スーパーキャット2013

1

だから私はここのパーティーに少し遅れる。:)

サブスクライバーなしでイベントを表すためのnullオブジェクトパターンではなくnullの使用については、このシナリオを検討してください。イベントを呼び出す必要がありますが、オブジェクト(EventArgs)の作成は簡単ではなく、一般的なケースでは、イベントにサブスクライバーがありません。引数を作成してイベントを呼び出す処理をコミットする前に、コードを最適化してサブスクライバーがまったくいないかどうかを確認できると便利です。

これを念頭に置いて、解決策は、「まあ、ゼロの加入者はnullで表される」と言うことです。次に、高価な操作を実行する前に、nullチェックを実行します。これを行う別の方法は、DelegateタイプにCountプロパティを設定することだったと思います。そのため、myDelegate.Count> 0の場合にのみ、高価な操作を実行します。Countプロパティの使用は、元の問題を解決するやや良いパターンです最適化を許可し、NullReferenceExceptionを発生させずに呼び出すことができるという優れた特性もあります。

ただし、デリゲートは参照型であるため、nullを許可されていることに注意してください。おそらく、この事実を隠蔽してイベントのnullオブジェクトパターンのみをサポートする良い方法は単になかったので、代替策として、開発者にnullとゼロのサブスクライバーの両方をチェックするように強制している可能性があります。それは現在の状況よりも醜いでしょう。

注:これは純粋な推測です。.NET言語やCLRには関与していません。


「...ではなく空のデリゲートを使用する」という意味だと思います。イベントを空のデリゲートに初期化することで、すでに提案したことを実行できます。テスト(MyEvent.GetInvocationList()。Length == 1)は、リストの最初の空のデリゲートが唯一のものである場合にtrueになります。最初にコピーを作成する必要はまだありません。あなたが説明するケースはとにかく非常にまれだと思いますが。
Daniel Earwicker、2009年

ここでは、デリゲートとイベントのアイデアを融合させていると思います。クラスにイベントFooがある場合、外部ユーザーがMyType.Foo + = /-=を呼び出すと、実際にはadd_Foo()およびremove_Foo()メソッドが呼び出されます。ただし、Fooが定義されているクラス内からFooを参照する場合、実際にはadd_Foo()およびremove_Foo()メソッドではなく、基になるデリゲートを直接参照しています。また、EventHandlerListのようなタイプが存在するため、デリゲートとイベントが同じ場所にあることを強制するものは何もありません。これは、私の回答の「心に留めておく」段落で私が意味したものです。
リーバイ

(続き)私はこれが紛らわしい設計であることを認めますが、代替案はもっと悪いかもしれません。結局あなたが持っているのはデリゲートだけなので-基礎となるデリゲートを直接参照するかもしれないし、コレクションからそれを得るかもしれないし、その場でそれをインスタンス化するかもしれない-それは「チェックnull」パターン。
リーバイ

イベントの発生について話しているので、ここで追加/削除アクセサーが重要である理由がわかりません。
Daniel Earwicker、2009

@Levi:C#がイベントを処理する方法が本当に嫌いです。もし私がドゥルーターを持っているなら、デリゲートはイベントとは違う名前を与えられていただろう。クラスの外部から、イベント名に対して許可される操作は、+=およびのみ-=です。クラス内で許可される操作には、呼び出し(組み込みのnullチェックを使用)、に対するテストnull、またはへの設定も含まれますnull。それ以外の場合は、特定のプレフィックスまたはサフィックスが付いたイベント名を名前とするデリゲートを使用する必要があります。
スーパーキャット2012年

0

シングルスレッドアプリケーションの場合、これは問題ではありません。

ただし、イベントを公開するコンポーネントを作成している場合、コンポーネントのコンシューマーがマルチスレッド化しないという保証はありません。その場合、最悪の事態に備える必要があります。

空のデリゲートを使用すると問題は解決しますが、イベントを呼び出すたびにパフォーマンスが低下し、GCに影響する可能性があります。

これが発生するために、コンシューマがサブスクライブを解除することは正しいですが、一時コピーを通過した場合は、メッセージがすでに転送中であることを考慮してください。

一時変数を使用せず、空のデリゲートを使用せず、誰かがサブスクライブを解除すると、null参照例外が発生します。これは致命的であるため、コストに見合う価値があると思います。


0

再利用可能なコンポーネントの静的メソッド(など)でこの種の潜在的なスレッド化の悪さから保護するだけであり、静的イベントを作成しないため、これをそれほど問題とは考えていません。

私はそれを間違っていますか?


変更可能な状態のクラスのインスタンス(値を変更するフィールド)を割り当て、複数のスレッドが同じインスタンスに同時にアクセスできるようにし、ロックを使用してそれらのフィールドが2つのスレッドによって同時に変更されないように保護している場合、おそらくそれは間違っています。すべてのスレッドに独自の個別のインスタンスがある(何も共有しない)場合、またはすべてのオブジェクトが不変である(いったん割り当てられると、それらのフィールドの値は変更されない)場合は、おそらく問題ありません。
Daniel Earwicker、2009

私の一般的なアプローチは、静的メソッドを除いて、呼び出し側に同期を任せることです。私が発信者である場合、その高いレベルで同期します。(もちろん、同期されたアクセスを処理することのみを目的とするオブジェクトを除きます。:))
Greg D

@GregDは、メソッドの複雑さと、使用するデータに依存します。それが内部メンバーに影響を及ぼし、スレッド/タスク状態で実行することを決定した場合、多くの損害を
被ります

0

建設中のすべてのイベントを配線し、そのままにしておきます。この投稿の最後の段落で説明するように、Delegateクラスの設計では、他の使用法を正しく処理できない可能性があります。

まず、イベントハンドラーが通知応答するかどうか、またはどのように応答するかについて同期した決定を既に行う必要がある場合、イベント通知インターセプトしようとしても意味がありません。

通知される可能性のあるものはすべて通知する必要があります。イベントハンドラーが通知を適切に処理している(つまり、信頼できるアプリケーションの状態にアクセスでき、適切な場合にのみ応答する)場合は、いつでも通知して、適切に応答すると信頼できます。

イベントが発生したことをハンドラーに通知してはならない唯一の場合は、イベントが実際に発生していない場合です!したがって、ハンドラーに通知を送信したくない場合は、イベントの生成を停止します(つまり、コントロールを無効にするか、最初にイベントを検出して存在させる役割を担うものをすべて無効にします)。

正直なところ、Delegateクラスは救済できないと思います。MulticastDelegateへのマージ/移行は、イベントの(有用な)定義を、ある瞬間に発生するものから、ある期間にわたって発生するものに効果的に変更したため、大きな間違いでした。このような変更には、論理的に1つの瞬間に戻すことができる同期メカニズムが必要ですが、MulticastDelegateにはそのようなメカニズムがありません。同期は、タイムスパン全体またはイベントが発生した瞬間を網羅する必要があります。これにより、アプリケーションは、イベントの処理を開始するという同期の決定を行うと、完全に(トランザクションで)処理を終了します。MulticastDelegate / Delegateハイブリッドクラスであるブラックボックスでは、これはほぼ不可能であるため、単一のサブスクライバーの使用に固執するか、ハンドラーチェーンが使用/変更されている間に取り出すことができる同期ハンドルを持つ独自の種類のMulticastDelegateを実装します。代替手段は、すべてのハンドラーに同期/トランザクション整合性を冗長的に実装することであり、それは途方もなく/不必要に複雑になるため、これをお勧めします。


[1]「1つの瞬間」で発生する便利なイベントハンドラーはありません。すべての操作にはタイムスパンがあります。単一のハンドラーは、実行する重要なステップのシーケンスを持つことができます。ハンドラーのリストをサポートしても何も変わりません。
Daniel Earwicker、2009年

[2]イベントが発生する間、ロックを保持することは完全な狂気です。それは必然的にデッドロックにつながります。ソースはロックAを取り出し、イベントを起動し、シンクはロックBを取り出し、2つのロックが保持されます。別のスレッドで何らかの操作を行った結果、ロックが逆の順序で取り出された場合はどうなりますか?ロックの責任が個別に設計/テストされたコンポーネント間で分割される場合(イベントの全体的なポイント)、そのような致命的な組み合わせをどのように除外することができますか?
Daniel Earwicker、2009年

[3]これらの問題のいずれも、コンポーネントのシングルスレッド構成、特にGUIフレームワークでの通常のマルチキャストデリゲート/イベントのすべてに広がる有用性を決して減少させません。この使用例は、イベントの使用の大部分をカバーしています。フリースレッドでイベントを使用することには疑問の余地があります。これは、デザインや、意味のある状況での明らかな有用性を無効にするものではありません。
ダニエルエリカー

[4]スレッド+同期イベントは、本質的にはレッドニシンです。待ち行列に入れられた非同期通信は、進むべき道です。
Daniel Earwicker、2009年

[1]私は測定された時間について言及していませんでした...論理的に瞬時に発生するアトミック操作について話していました...つまり、イベントの発生中に、使用している同じリソースに関係する他の何も変更できないことを意味しますロック付きでシリアル化されているためです。
Triynko、2009年

0

こちらをご覧ください:http : //www.danielfortunov.com/software/%24daniel_fortunovs_adventures_in_software_development/2009/04/23/net_event_invocation_thread_safety これは正しいソリューションであり、他のすべての回避策の代わりに常に使用する必要があります。

「何もしない匿名メソッドで初期化することにより、内部呼び出しリストに常に少なくとも1つのメンバーが含まれるようにすることができます。外部の当事者は匿名メソッドへの参照を持つことができないため、外部の当事者はメソッドを削除できないため、デリゲートがnullになることはありません。— .NETコンポーネントのプログラミング、第2版、JuvalLöwy

public static event EventHandler<EventArgs> PreInitializedEvent = delegate { };  

public static void OnPreInitializedEvent(EventArgs e)  
{  
    // No check required - event will never be null because  
    // we have subscribed an empty anonymous delegate which  
    // can never be unsubscribed. (But causes some overhead.)  
    PreInitializedEvent(null, e);  
}  

0

質問がc#の「イベント」タイプに限定されているとは思わない。その制限を取り除いて、ホイールを少し再発明して、これらの線に沿って何かをしてみませんか?

イベントスレッドを安全に上げる-ベストプラクティス

  • レイズ内で任意のスレッドをサブスクライブ/サブスクライブ解除する機能(競合状態が削除されます)
  • クラスレベルでの+ =および-=の演算子のオーバーロード
  • 一般的な呼び出し元定義のデリゲート

0

有益な議論をありがとう。私は最近この問題に取り組んでおり、少し遅いが、破棄されたオブジェクトへの呼び出しを回避できるようにする次のクラスを作成しました。

ここでの主なポイントは、イベントが発生した場合でも呼び出しリストを変更できることです。

/// <summary>
/// Thread safe event invoker
/// </summary>
public sealed class ThreadSafeEventInvoker
{
    /// <summary>
    /// Dictionary of delegates
    /// </summary>
    readonly ConcurrentDictionary<Delegate, DelegateHolder> delegates = new ConcurrentDictionary<Delegate, DelegateHolder>();

    /// <summary>
    /// List of delegates to be called, we need it because it is relatevely easy to implement a loop with list
    /// modification inside of it
    /// </summary>
    readonly LinkedList<DelegateHolder> delegatesList = new LinkedList<DelegateHolder>();

    /// <summary>
    /// locker for delegates list
    /// </summary>
    private readonly ReaderWriterLockSlim listLocker = new ReaderWriterLockSlim();

    /// <summary>
    /// Add delegate to list
    /// </summary>
    /// <param name="value"></param>
    public void Add(Delegate value)
    {
        var holder = new DelegateHolder(value);
        if (!delegates.TryAdd(value, holder)) return;

        listLocker.EnterWriteLock();
        delegatesList.AddLast(holder);
        listLocker.ExitWriteLock();
    }

    /// <summary>
    /// Remove delegate from list
    /// </summary>
    /// <param name="value"></param>
    public void Remove(Delegate value)
    {
        DelegateHolder holder;
        if (!delegates.TryRemove(value, out holder)) return;

        Monitor.Enter(holder);
        holder.IsDeleted = true;
        Monitor.Exit(holder);
    }

    /// <summary>
    /// Raise an event
    /// </summary>
    /// <param name="args"></param>
    public void Raise(params object[] args)
    {
        DelegateHolder holder = null;

        try
        {
            // get root element
            listLocker.EnterReadLock();
            var cursor = delegatesList.First;
            listLocker.ExitReadLock();

            while (cursor != null)
            {
                // get its value and a next node
                listLocker.EnterReadLock();
                holder = cursor.Value;
                var next = cursor.Next;
                listLocker.ExitReadLock();

                // lock holder and invoke if it is not removed
                Monitor.Enter(holder);
                if (!holder.IsDeleted)
                    holder.Action.DynamicInvoke(args);
                else if (!holder.IsDeletedFromList)
                {
                    listLocker.EnterWriteLock();
                    delegatesList.Remove(cursor);
                    holder.IsDeletedFromList = true;
                    listLocker.ExitWriteLock();
                }
                Monitor.Exit(holder);

                cursor = next;
            }
        }
        catch
        {
            // clean up
            if (listLocker.IsReadLockHeld)
                listLocker.ExitReadLock();
            if (listLocker.IsWriteLockHeld)
                listLocker.ExitWriteLock();
            if (holder != null && Monitor.IsEntered(holder))
                Monitor.Exit(holder);

            throw;
        }
    }

    /// <summary>
    /// helper class
    /// </summary>
    class DelegateHolder
    {
        /// <summary>
        /// delegate to call
        /// </summary>
        public Delegate Action { get; private set; }

        /// <summary>
        /// flag shows if this delegate removed from list of calls
        /// </summary>
        public bool IsDeleted { get; set; }

        /// <summary>
        /// flag shows if this instance was removed from all lists
        /// </summary>
        public bool IsDeletedFromList { get; set; }

        /// <summary>
        /// Constuctor
        /// </summary>
        /// <param name="d"></param>
        public DelegateHolder(Delegate d)
        {
            Action = d;
        }
    }
}

そして使い方は:

    private readonly ThreadSafeEventInvoker someEventWrapper = new ThreadSafeEventInvoker();
    public event Action SomeEvent
    {
        add { someEventWrapper.Add(value); }
        remove { someEventWrapper.Remove(value); }
    }

    public void RaiseSomeEvent()
    {
        someEventWrapper.Raise();
    }

テスト

以下の方法でテストしました。私はこのようなオブジェクトを作成して破壊するスレッドを持っています:

var objects = Enumerable.Range(0, 1000).Select(x => new Bar(foo)).ToList();
Thread.Sleep(10);
objects.ForEach(x => x.Dispose());

Bar(リスナーオブジェクト)コンストラクタIはを購読SomeEvent(上記のように実施される)中に退会しますDispose

    public Bar(Foo foo)
    {
        this.foo = foo;
        foo.SomeEvent += Handler;
    }

    public void Handler()
    {
        if (disposed)
            Console.WriteLine("Handler is called after object was disposed!");
    }

    public void Dispose()
    {
        foo.SomeEvent -= Handler;
        disposed = true;
    }

また、ループでイベントを発生させるスレッドがいくつかあります。

これらのアクションはすべて同時に実行されます。多くのリスナーが作成および破棄され、同時にイベントが発生します。

競合状態があった場合、コンソールにメッセージが表示されるはずですが、空です。しかし、いつものようにclrイベントを使用すると、警告メッセージがいっぱい表示されます。したがって、c#でスレッドセーフイベントを実装することが可能であると結論付けることができます。

どう思いますか?


私には十分に見えます。(理論的には)がテストアプリケーションのdisposed = truefoo.SomeEvent -= Handlerに発生し、誤検知が発生する可能性があると思います。しかし、それとは別に、変更したいことがいくつかあります。あなたは本当にtry ... finallyロックに使いたいのです-これはこれをスレッドセーフにするだけでなく、アボートセーフにするのにも役立ちます。言うまでもなく、そのばかげたことを取り除くことができますtry catch。そして、あなたはAdd/に渡されたデリゲートをチェックしていませんRemove-それは可能性がありますnullAdd/にすぐに投げるべきですRemove)。
Luaan
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.