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

Jér*_*and 38 c# event-handling

为了引发事件,我们使用OnEventName方法,如下所示:

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

但这个有什么不同?

protected virtual void OnSomethingHappened(EventArgs e) 
{
    if (SomethingHappened!= null) 
    {
        SomethingHappened(this, e);
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

Fre*_*örk 51

有一个微小的机会SomethingHappened成为null空校验之后,但在调用之前.然而,MulticastDelagates为不可变的,所以如果你第一次分配一个变量,空检查依据的变量,并通过它调用,你是从场景中安全(自我插头:我写了一个博客张贴关于这一段时间以前).

虽然有硬币的背面; 如果使用临时变量方法,则代码受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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

使用该方法,您可以调用以下事件:

protected void OnSomeEvent(EventArgs e)
{
    SomeEvent.SafeInvoke(this, e);
}
Run Code Online (Sandbox Code Playgroud)

  • 值得一提的是,使用C#6.0,我们现在可以使用"elvis运算符"(也称为空传播运算符)来安全地引发事件.`SomeEvent?.Invoke(sender,args);` (3认同)
  • 你引用的博客文章中有一个错误:你的怪癖解释中的最后一个词应该是'分离'而不是'附加'. (2认同)

Krz*_*cki 33

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

SomethingHappened?.Invoke(this, args);
Run Code Online (Sandbox Code Playgroud)

它是线程安全的,因为它只评估左侧一次,并将其保存在临时变量中.您可以在这里阅读更多部分标题为Null-conditional运算符.

更新: 实际上Visual Studio 2015的更新2现在包含重构,以简化委托调用,最终将使用这种类型的表示法.您可以在本公告中阅读相关内容.


Jes*_*cer 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);
        }
    }
Run Code Online (Sandbox Code Playgroud)

  • 是的,你和我意见一致。接受的答案有一个微妙的内存障碍问题,我们的解决方案解决了这个问题。使用自定义的“add”和“remove”处理程序可能是不必要的,因为编译器会在自动实现中发出锁。不过,我想我记得 .NET 4.0 中发生了一些变化。 (2认同)

rpe*_*kov 12

对于.NET 4.5,最好使用它Volatile.Read来分配临时变量.

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

更新:

本文对此进行了解释: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);
    }
}
Run Code Online (Sandbox Code Playgroud)

将编译成这样:

protected virtual void OnSomethingHappened(EventArgs e) 
{
    if (SomethingHappened != null) 
    {
        SomethingHappened(this, e);
    }
}
Run Code Online (Sandbox Code Playgroud)

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


jga*_*fin 7

声明这样的事件以获得线程安全:

public event EventHandler<MyEventArgs> SomethingHappened = delegate{};
Run Code Online (Sandbox Code Playgroud)

并像这样调用它:

protected virtual void OnSomethingHappened(MyEventArgs e)   
{  
    SomethingHappened(this, e);
} 
Run Code Online (Sandbox Code Playgroud)

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

  • 这是一个非常小的开销.您的代码中存在应该首先优化的更大问题. (8认同)

Bri*_*eon 6

这取决于线程安全的含义.如果您的定义仅包括预防,NullReferenceException则第一个示例安全.不过,如果你有一个更严格的定义去该事件处理程序必须进行,如果他们再调用存在既不是安全的.原因与内存模型和障碍的复杂性有关.这可能是有,事实上,事件处理程序链接到委托,但该线程总是读基准为空.修复两者的正确方法是在将委托引用捕获到局部变量的点处创建显式内存屏障.有几种方法可以做到这一点.

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

尽管尴尬的范围问题阻止你做单行初始化程序,但我仍然更喜欢这种lock方法.

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

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