2010-09-08 71 views
32

爲了提高我們使用的方法的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); 
    } 
} 

顯然第一個是線程安全的,但爲什麼以及如何?

沒有必要開始一個新的線程?

回答

47

在空檢查之後但在調用之前,有一個很小的機會SomethingHappened變爲null。然而,MulticastDelagate是不可變的,所以如果你首先分配一個變量,對變量進行null檢查並通過它進行調用,那麼對於這種情況你是安全的(自我插件:前一段我寫了blog post about this)。

儘管有硬幣的背面,如果您使用臨時變量方法,則您的代碼將受到保護而不受NullReferenceException的影響,但可能是事件將從事件中分離後調用事件偵聽器。這只是可能以最優雅的方式處理的事情。

爲了解決這個問題我有,我有時會使用擴展方法:其實

protected void OnSomeEvent(EventArgs e) 
{ 
    SomeEvent.SafeInvoke(this, e); 
} 
+0

感謝您的傑出答案(和博客文章)。 – 2010-09-08 15:13:55

+1

我在我的核心庫中也有這個擴展方法,名字完全一樣,用完全相同的方法完成相同的工作!雖然我的參數名稱是eventHandler。 – tia 2010-09-08 19:19:30

+0

-1:'雖然有硬幣的背面,如果使用臨時變量方法(...),則可能是事件在事件從事件中分離後會調用事件偵聽器:這是**總是**的可能性;這是不可避免的。 – ANeves 2014-02-20 11:57:41

7

聲明你這樣的活動來獲得線程安全:

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

並調用它像這樣:

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

雖然不再需要的方法..

+1

如果您有很多事件觸發,這可能會導致一些性能問題,因爲每次都會執行空的委託,無論是否有合法的訂閱者參與事件。這是一個小的開銷,但理論上可以加起來。 – 2010-09-08 15:04:13

+7

這是一個非常小的開銷。你的代碼中有更大的問題需要首先優化。 – jgauffin 2010-09-08 17:37:00

1

其實,沒有,第二個例子不被認爲是線程安全的。 SomethingHappened事件可以在條件中評估爲非null,然後在調用時爲null。這是一個經典的競賽條件。

3

,第一:

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

使用這種方法,你可以調用這樣的活動是線程安全的,但第二個不是。第二個問題是SomethingHappened委託可以在空驗證和調用之間更改爲null。有關更完整的解釋,請參閱http://blogs.msdn.com/b/ericlippert/archive/2009/04/29/events-and-races.aspx

0

對於其中任何一個線程安全的情況,假設所有訂閱該事件的對象都是線程安全的。

6

這取決於你的意思是線程安全的。如果你的定義只包括預防NullReferenceException那麼第一個例子是更多安全。然而,如果你使用更嚴格的定義,其中事件處理程序必須被調用(如果它們存在),那麼都不是是安全的。原因與記憶模型和障礙的複雜性有關。實際上,事件處理程序可能會鏈接到委託,但該線程始終將引用讀爲null。解決這兩個問題的正確方法是在代理引用被捕獲到本地變量的位置創建顯式內存屏障。有幾種方法可以做到這一點。

  • 使用lock關鍵字(或任何同步機制)。
  • 在事件變量上使用volatile關鍵字。
  • 使用Thread.MemoryBarrier

儘管阻礙你做單線初始化的尷尬範圍問題,我仍然更喜歡lock方法。

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

需要注意的是在這種特殊情況下的內存屏障問題很可能是沒有實際意義,因爲它是不太可能的變數,將讀取外面的方法調用被取消是很重要的。但是,如果編譯器決定內聯該方法,則沒有特別的保證。

12

我把這個片段四周,用於設定和發射安全事件的多線程訪問的參考:

/// <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); 
     } 
    } 
+1

是的,你和我在同一頁面上。接受的答案有我們的解決方案解決的一個微妙的記憶障礙問題。使用自定義的'add'和'remove'處理程序可能是不必要的,因爲編譯器在自動實現中發出鎖。雖然,我想我記得在.NET 4.0中改變了一些東西。 – 2010-09-08 16:21:36

+0

@Brian - 雖然在4.0之前就已經同意,鎖在'this'對象上,這意味着類外部的代碼可以通過鎖定實例來阻止機制。 Jon Skeet在這裏提供了靈感http://csharpindepth.com/Articles/Chapter2/Events.aspx#threading。 – 2010-09-08 16:42:11

+0

偉大的鏈接。是的,我確認了所有的細節,並鎖定了「this」。任何人都可以快速鏈接到.NET 4.0中的更改?如果沒有,我只會拉起規範。 – 2010-09-08 17:43:02

9

對於.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#」中有所解釋。

主要思想是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); 
    } 
} 

這發生在某些特殊情況下,但它可能發生。

+0

我不知道使用揮發性。你能解釋一下爲什麼它更好? – 2013-06-18 11:33:26

+0

對我的回答添加了解釋。 – rpeshkov 2013-06-18 16:23:29

+0

謝謝,很高興知道。 – 2013-06-18 17:36:16

1

我試圖皮條客出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; 
     }   
    } 

此方法的任何主要問題?

該代碼僅在插入時進行了簡短的測試和編輯。
預先確認列表<>如果有很多元素,這不是一個很好的選擇。

26

由於C#6.0可以使用monadic空條件運算符?.以簡單和線程安全的方式檢查null和raise事件。

SomethingHappened?.Invoke(this, args); 

它是線程安全的,因爲它只評估一次左側,並將其保留在臨時變量中。您可以在部分標題爲空條件運算符中閱讀更多here

更新: 實際更新2爲Visual Studio 2015年現在包含重構簡化委託調用,將結束與正好這類符號。你可以在這announcement閱讀有關它。

+0

感謝您的更新,總是很高興知道。 – 2015-09-06 08:29:14