1. ホーム
  2. c#

[解決済み】C#のイベントとスレッドセーフについて

2022-04-03 12:26:52

質問

アップデイト

C#6時点では。 答え となっています。

SomeEvent?.Invoke(this, e);


よく次のようなアドバイスを聞いたり読んだりします。

をチェックする前に、必ずイベントのコピーを作成します。 null を実行します。これにより、スレッド化した場合に起こりうる、イベントが null を使用すると、NULLチェックとイベント実行のちょうど中間に位置することになります。

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

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

更新 : 最適化について読んでいて、これもイベントメンバをvolatileにする必要があるのではないかと思ったのですが、Jon Skeetは回答で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例外は、コンポーネントの不正使用を検出するのに役立ちます。もし、コンポーネントを悪用から守りたいのであれば、WPFの例に従って、コンストラクタにスレッドIDを保存し、他のスレッドがコンポーネントと直接対話しようとしたら例外を投げるようにすればよいのです。あるいは、本当にスレッドセーフなコンポーネントを実装することです(簡単なことではありません)。

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

Eric Lippertのブログ記事を受けて更新。

イベントハンドラは、イベントの登録が解除された後でも呼び出されることに対して堅牢であることが要求されるため、明らかにイベントデリゲートが null . イベントハンドラに関するそのような要件は、どこかに文書化されているのでしょうか?

例えば、ハンドラを初期化して、削除されない空のアクションを持たせるなどです。しかし、NULLチェックを行うのが標準的なパターンです。

というわけで、私の質問の残り1つの断片は。 なぜ explicit-null-check が "標準パターン" なのでしょうか? 空のデリゲートを割り当てるという代替案では、必要なのは以下の通りです。 = delegate {} をイベント宣言に追加することで、イベントが発生するあらゆる場所から、あの臭いセレモニーの小山を排除することができるのです。空のデリゲートがインスタンス化し安いことを確認するのは簡単でしょう。それとも、私はまだ何かを見逃しているのだろうか?

きっと、(Jon Skeetが示唆したように)これは.NET 1.xのアドバイスに過ぎず、2005年にそうなるべきだったのに、まだ滅びていないのでしょうか。

解決するには?

最初のほうで言っている最適化をJITが行うことは、条件的に許されないんだ。ちょっと前に妖怪として話題になりましたが、有効ではありませんね。(しばらく前にJoe DuffyかVance Morrisonのどちらかに確認しましたが、どちらか覚えていません)。

volatile修飾子がなければ、取られたローカルコピーが古くなる可能性はありますが、それだけです。この場合 NullReferenceException .

そして、確かにレースコンディションは存在します。しかし、常に存在するものなのです。仮に、このコードを次のように変更したとしましょう。

TheEvent(this, EventArgs.Empty);

ここで、そのデリゲートの呼び出しリストが1000件あるとします。他のスレッドがリストの末尾にあるハンドラの登録を解除する前に、リストの先頭にあるアクションが実行されることは完全にあり得ます。しかし、そのハンドラはまだ新しいリストなので、実行されます。(Delegateは不変です。) 私が見る限り、これは避けられないことです。

空のデリゲートを使うことで、確かにNullityチェックは回避できますが、レースコンディションを解決することはできません。また、常に変数の最新の値を見ることができるわけではありません。