web-dev-qa-db-ja.com

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

イベントを発生させるには、次のようなOnEventNameメソッドを使用します。

protected virtual void OnSomethingHappened(EventArgs e) 
{
    EventHandler handler = SomethingHappened;
    if (handler != null) 
    {
        handler(this, e);
    }
}

しかし、これとの違いは何ですか?

protected virtual void OnSomethingHappened(EventArgs e) 
{
    if (SomethingHappened!= null) 
    {
        SomethingHappened(this, e);
    }
}

どうやら最初はスレッドセーフですが、なぜそしてどのように?

新しいスレッドを開始する必要はありませんか?

40

Nullチェックの後、呼び出しの前にSomethingHappenednullになる可能性はわずかです。ただし、MulticastDelagatesは不変であるため、最初に変数を割り当て、変数に対してnullチェックを行い、それを介して呼び出す場合は、そのシナリオから安全です(セルフプラグ:私は これについてのブログ投稿) 少し前)。

コインの裏側もあります。一時変数アプローチを使用する場合、コードはNullReferenceExceptionsから保護されますが、イベントから切り離された後、イベントがイベントリスナーを呼び出す可能性があります。これは、可能な限り最も優雅な方法で対処するためのものです。

これを回避するために、私は時々使用する拡張メソッドを持っています:

public static class EventHandlerExtensions
{
    public static void SafeInvoke<T>(this EventHandler<T> evt, object sender, T e) where T : EventArgs
    {
        if (evt != null)
        {
            evt(sender, e);
        }
    }
}

そのメソッドを使用して、次のようなイベントを呼び出すことができます。

protected void OnSomeEvent(EventArgs e)
{
    SomeEvent.SafeInvoke(this, e);
}
53
Fredrik Mörk

C#6.0以降では、モナディックNull条件演算子?.を使用してnullを確認し、簡単でスレッドセーフな方法でイベントを発生させることができます。

SomethingHappened?.Invoke(this, args);

左側を1回だけ評価し、それを一時変数に保持するため、スレッドセーフです。あなたはもっと読むことができます ここ 部分的にNull-conditional operatorというタイトルで。

Update:実際、Visual Studio 2015のUpdate 2には、このタイプの表記で終わるデリゲートの呼び出しを簡略化するリファクタリングが含まれています。これについては 発表 で読むことができます。

35

私はこのスニペットを、設定と起動の両方のための安全なマルチスレッドイベントアクセスのリファレンスとして保持します。

    /// <summary>
    /// Lock for SomeEvent delegate access.
    /// </summary>
    private readonly object someEventLock = new object();

    /// <summary>
    /// Delegate variable backing the SomeEvent event.
    /// </summary>
    private EventHandler<EventArgs> someEvent;

    /// <summary>
    /// Description for the event.
    /// </summary>
    public event EventHandler<EventArgs> SomeEvent
    {
        add
        {
            lock (this.someEventLock)
            {
                this.someEvent += value;
            }
        }

        remove
        {
            lock (this.someEventLock)
            {
                this.someEvent -= value;
            }
        }
    }

    /// <summary>
    /// Raises the OnSomeEvent event.
    /// </summary>
    public void RaiseEvent()
    {
        this.OnSomeEvent(EventArgs.Empty);
    }

    /// <summary>
    /// Raises the SomeEvent event.
    /// </summary>
    /// <param name="e">The event arguments.</param>
    protected virtual void OnSomeEvent(EventArgs e)
    {
        EventHandler<EventArgs> handler;

        lock (this.someEventLock)
        {
            handler = this.someEvent;
        }

        if (handler != null)
        {
            handler(this, e);
        }
    }
14
Jesse C. Slicer

.NET 4.5の場合、Volatile.Readを使用して一時変数を割り当てることをお勧めします。

protected virtual void OnSomethingHappened(EventArgs e) 
{
    EventHandler handler = Volatile.Read(ref SomethingHappened);
    if (handler != null) 
    {
        handler(this, e);
    }
}

更新:

これについては、この記事で説明しています: http://msdn.Microsoft.com/en-us/magazine/jj883956.aspx 。また、「CLR via C#」の第4版で説明されました。

主なアイデアは、JITコンパイラがコードを最適化し、ローカルの一時変数を削除できるということです。したがって、このコード:

protected virtual void OnSomethingHappened(EventArgs e) 
{
    EventHandler handler = SomethingHappened;
    if (handler != null) 
    {
        handler(this, e);
    }
}

これにコンパイルされます:

protected virtual void OnSomethingHappened(EventArgs e) 
{
    if (SomethingHappened != null) 
    {
        SomethingHappened(this, e);
    }
}

これは特定の特殊な状況で発生しますが、発生する可能性があります。

12
rpeshkov

スレッドセーフティを取得するには、次のようにイベントを宣言します。

public event EventHandler<MyEventArgs> SomethingHappened = delegate{};

そして、次のように呼び出します。

protected virtual void OnSomethingHappened(MyEventArgs e)   
{  
    SomethingHappened(this, e);
} 

メソッドはもう必要ありませんが。

7
jgauffin

それは、スレッドセーフの意味によって異なります。定義にNullReferenceExceptionの防止のみが含まれている場合、最初の例はmore安全です。ただし、イベントハンドラーが存在する場合にイベントハンドラーmustを呼び出すというより厳密な定義を使用する場合は、どちらでもないが安全です。その理由は、記憶モデルと障壁の複雑さに関係しています。実際には、デリゲートにチェーンされたイベントハンドラーが存在する可能性がありますが、スレッドは常に参照をnullとして読み取ります。両方を修正する正しい方法は、デリゲート参照がローカル変数にキャプチャされた時点で明示的なメモリバリアを作成することです。これにはいくつかの方法があります。

  • lockキーワード(または任意の同期メカニズム)を使用します。
  • イベント変数でvolatileキーワードを使用します。
  • 使用する Thread.MemoryBarrier

1行のイニシャライザを実行できない厄介なスコープの問題にもかかわらず、私はlockメソッドを好んでいます。

protected virtual void OnSomethingHappened(EventArgs e)           
{          
    EventHandler handler;
    lock (this)
    {
      handler = SomethingHappened;
    }
    if (handler != null)           
    {          
        handler(this, e);          
    }          
}          

この特定のケースでは、変数の読み取りがメソッド呼び出しの外で持ち上げられる可能性が低いため、メモリバリアの問題はおそらく意味がないことに注意することが重要です。ただし、コンパイラーがメソッドをインライン化することを決定した場合、特に保証はありません。

7
Brian Gideon

実際、1つ目はスレッドセーフですが、2つ目はスレッドセーフではありません。 2番目の問題は、Null検証と呼び出しの間でSomethingHappenedデリゲートがnullに変更される可能性があることです。より完全な説明については、 http://blogs.msdn.com/b/ericlippert/archive/2009/04/29/events-and-races.aspx を参照してください。

3
Nicole Calinoiu

実際、いいえ、2番目の例はスレッドセーフとは見なされません。 SomethingHappenedイベントは、条件でnull以外と評価され、呼び出されたときにnullになる可能性があります。それは古典的なレースコンディションです。

1
Curt Nichols

私はピンプアウトしようとした Jesse C. Slicer の答え:

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

    public class ThreadSafeEventDispatcher<T> where T : class
    {
        readonly object _lock = new object();
    
        private class RemovableDelegate
        {
            public readonly T Delegate;
            public bool RemovedDuringRaise;
    
            public RemovableDelegate(T @delegate)
            {
                Delegate = @delegate;
            }
        };
    
        List<RemovableDelegate> _delegates = new List<RemovableDelegate>();
    
        Int32 _raisers;  // indicate whether the event is being raised
    
        // Raises the Event
        public void Raise(Func<T, bool> raiser)
        {
            try
            {
                List<RemovableDelegate> raisingDelegates;
                lock (_lock)
                {
                    raisingDelegates = new List<RemovableDelegate>(_delegates);
                    _raisers++;
                }
    
                foreach (RemovableDelegate d in raisingDelegates)
                {
                    lock (_lock)
                        if (d.RemovedDuringRaise)
                            continue;
    
                    raiser(d.Delegate);  // Could use return value here to stop.                    
                }
            }
            finally
            {
                lock (_lock)
                    _raisers--;
            }
        }
    
        // Override + so that += works like events.
        // Adds are not recognized for any event currently being raised.
        //
        public static ThreadSafeEventDispatcher<T> operator +(ThreadSafeEventDispatcher<T> tsd, T @delegate)
        {
            lock (tsd._lock)
                if (!tsd._delegates.Any(d => d.Delegate == @delegate))
                    tsd._delegates.Add(new RemovableDelegate(@delegate));
            return tsd;
        }
    
        // Override - so that -= works like events.  
        // Removes are recongized immediately, even for any event current being raised.
        //
        public static ThreadSafeEventDispatcher<T> operator -(ThreadSafeEventDispatcher<T> tsd, T @delegate)
        {
            lock (tsd._lock)
            {
                int index = tsd._delegates
                    .FindIndex(h => h.Delegate == @delegate);
    
                if (index >= 0)
                {
                    if (tsd._raisers > 0)
                        tsd._delegates[index].RemovedDuringRaise = true; // let raiser know its gone
    
                    tsd._delegates.RemoveAt(index); // okay to remove, raiser has a list copy
                }
            }
    
            return tsd;
        }
    }
    

使用法:

    class SomeClass
    {   
        // Define an event including signature
        public ThreadSafeEventDispatcher<Func<SomeClass, bool>> OnSomeEvent = 
                new ThreadSafeEventDispatcher<Func<SomeClass, bool>>();

        void SomeMethod() 
        {
            OnSomeEvent += HandleEvent; // subscribe

            OnSomeEvent.Raise(e => e(this)); // raise
        }

        public bool HandleEvent(SomeClass someClass) 
        { 
            return true; 
        }           
    }

このアプローチに大きな問題はありますか?

コードは簡単にテストされ、挿入時に少し編集されました。
多くの要素がある場合、List <>はあまり適切ではないことを事前に確認します。

1
crokusek

これらのいずれかがスレッドセーフであるためには、イベントにサブスクライブするすべてのオブジェクトもスレッドセーフであると想定しています。

0
Martin Brown