web-dev-qa-db-ja.com

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

UPDATE

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

更新:最適化について読むと、イベントメンバーも揮発性である必要があると考えましたが、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は、著者が購読を解除した後に実行されますが、それを防ぐために、特に購読を解除しただけです。本当に必要なのは、addおよびremoveアクセサーで適切な同期を行うカスタムイベントの実装です。さらに、イベントの発生中にロックが保持されると、デッドロックが発生する可能性があるという問題があります。

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

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

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

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

Eric Lippertのブログ投稿への応答で更新:

そのため、イベントハンドラーについて見逃した主なものがあります。「イベントハンドラーは、イベントがサブスクライブ解除された後でも呼び出されることに対して堅牢である必要がある」ため、明らかにイベントの可能性にのみ注意する必要がありますデリゲートはnullです。 イベントハンドラーの要件はどこに文書化されていますか?

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

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

確かに(Jon Skeetが示唆したように)これは、2005年に行われるはずだったように、消滅していない単なる.NET 1.xのアドバイスであるに違いない。

229

JITは、条件のために、最初の部分で述べている最適化を実行することはできません。私はこれが少し前に妖怪として提起されたことを知っていますが、それは無効です。 (私は少し前にジョー・ダフィーかヴァンス・モリソンのどちらかでそれをチェックしました;私はどれを思い出せません。)

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

そして、はい、確かに競合状態があります-しかし、常にあります。コードを次のように変更するとします。

TheEvent(this, EventArgs.Empty);

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

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

98
Jon Skeet

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

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() );

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

50
JP Alioto

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

この理由は、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イベント呼び出しスレッドの安全性 をご覧ください。

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

32
Daniel Fortunov

私は本当にこの読書を楽しんだ-ではない!イベントと呼ばれる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-競合状態の問題)Ericのブログ投稿を読んで、H(ハンドラー)がそれ自体を逆参照するときに処理する必要があることに同意しますが、イベントを不変/スレッドセーフにすることはできませんか? IE、作成時にロックフラグを設定して、呼び出されるたびに、実行中にすべてのサブスクライブとサブスクライブ解除をロックしますか?

結論

現代の言語は、このような問題を私たちのために解決することになっているのではないでしょうか?

11
Chuck Savage

本のジェフリー・リヒターによると C#経由のCLR 、正しい方法は次のとおりです。

// 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);

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

5
alyx

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

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);
  }
}

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

4
Ash

C#6以降では、次のように新しい.? operatorを使用してコードを簡素化できます。

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

リファレンス: https://docs.Microsoft.com/en-us/dotnet/csharp/language-reference/operators/null-conditional-operators

3
Phil1970

だから私はここでのパーティーに少し遅れています。 :)

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

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

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

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

1
Levi

このプラクティスは、特定の順序の操作を強制することではありません。実際には、null参照例外を回避することです。

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

注:競合状態を修正するには、おそらくハンドラーを実行するかどうかの同期フラグトラックを使用する必要があります

1
dss539

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

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

  • レイズ中に任意のスレッドからサブスクライブ/サブスクライブ解除する機能(競合状態が削除されました)
  • クラスレベルでの+ =および-=の演算子オーバーロード。
  • 呼び出し元が定義した汎用デリゲート
0
crokusek

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

まず第一に、interceptイベントを試みることには意味がありませんnotification when your eventhandlersは、通知に応答するかどうか/どのように行うかについて、すでに同期した決定を下す必要があります。

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

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

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

0
Triynko

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

「何もしない匿名メソッドで初期化することで、内部呼び出しリストに常に少なくとも1つのメンバーが含まれることを確認できます。外部のパーティは匿名メソッドへの参照を持つことができないため、外部のパーティはメソッドを削除できず、デリゲートは決してnullになりません」— 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
Eli

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

私は間違っていますか?

0
Greg D

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

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

/// <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(リスナーオブジェクト)コンストラクターで、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#で実装することが可能であると結論付けることができます。

どう思いますか?

0
Tony

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

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

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

消費者がこれを実現するために配信を停止することは正しいことですが、一時コピーを過ぎて配信を行った場合は、すでに送信中のメッセージを考慮してください。

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

0
Jason Coyne