安全地提升事件线程 - 最佳实践

时间:2010-09-08 14:48:54

标签: c# event-handling

为了引发事件,我们使用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);
    }
}

显然第一个是线程安全的,但为什么以及如何?

没有必要开始一个新线程吗?

10 个答案:

答案 0 :(得分:50)

在空检查之后但在调用之前,SomethingHappened变为null的可能性很小。但是,MulticastDelagate是不可变的,所以如果你首先分配一个变量,对变量进行空检查并通过它调用,你就可以安全地避开那个场景(自我插件:我刚才写了blog post about this )。

虽然有硬币的背面;如果使用临时变量方法,则代码将受到NullReferenceException的保护,但可能是事件将在事件分离后调用事件侦听器。这只是以最优雅的方式处理的事情。

为了解决这个问题,我有一个有时使用的扩展方法:

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

答案 1 :(得分:31)

从C#6.0开始,您可以使用monadic Null条件运算符?.来检查null并以简单且线程安全的方式引发事件。

SomethingHappened?.Invoke(this, args);

它是线程安全的,因为它仅评估左侧一次,并将其保存在临时变量中。您可以在标题为Null-conditional operators的部分中阅读更多here

<强>更新 实际上,Visual Studio 2015的Update 2现在包含重构以简化委托调用,最终将使用这种类型的表示法。您可以在此announcement中阅读相关内容。

答案 2 :(得分:13)

我将此代码段保留为设置和触发安全多线程事件访问的参考:

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

答案 3 :(得分:11)

对于.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);
    }
}

这种情况在某些特殊情况下会发生,但可能会发生。

答案 4 :(得分:7)

声明你的事件,以获得线程安全:

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

并像这样调用它:

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

虽然不再需要这种方法..

答案 5 :(得分:6)

这取决于线程安全的含义。如果您的定义仅包括NullReferenceException的预防,那么第一个示例 more 是安全的。但是,如果您使用更严格的定义,在该定义中,如果事件处理程序必须,则 是不安全的。原因与内存模型和障碍的复杂性有关。事实上,事件处理程序可能会链接到委托,但线程始终将引用读取为null。修复两者的正确方法是在将委托引用捕获到局部变量时创建显式内存屏障。有几种方法可以做到这一点。

  • 使用lock关键字(或任何同步机制)。
  • 在事件变量上使用volatile关键字。
  • 使用Thread.MemoryBarrier

尽管尴尬的范围问题导致您无法使用单行初始化程序,但我仍然更喜欢lock方法。

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

重要的是要注意,在这种特定情况下,内存屏障问题可能没有实际意义,因为在方法调用之外不可能取消变量的读取。但是,如果编译器决定内联方法,则没有任何保证。

答案 6 :(得分:3)

实际上,第一个是线程安全的,但第二个不是。第二个问题是,在null验证和调用之间可以将SomethingHappened委托更改为null。有关更完整的说明,请参阅http://blogs.msdn.com/b/ericlippert/archive/2009/04/29/events-and-races.aspx

答案 7 :(得分:1)

实际上,不,第二个例子不被认为是线程安全的。 SomethingHappened事件可以在条件中计算为非null,然后在调用时为null。这是一个典型的竞争条件。

答案 8 :(得分: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; 
        }           
    }

这种方法有什么重大问题吗?

代码只是在插入时进行了简单的测试和编辑 预先确认列表&lt;&gt;如果有很多元素,那就不是一个很好的选择。

答案 9 :(得分:0)

对于其中任何一个是线程安全的,您假设订阅该事件的所有对象也是线程安全的。