简体   繁体   English

安全地引发事件线程-最佳实践

[英]Raise event thread safely - best practice

In order to raise an event we use a method OnEventName like this: 为了引发事件,我们使用如下方法OnEventName:

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

But what is the difference with this one ? 但是,这有什么区别?

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

Apparently the first is thread-safe, but why and how ? 显然第一个是线程安全的,但是为什么以及如何执行?

It's not necessary to start a new thread ? 不必启动新线程吗?

There is a tiny chance that SomethingHappened becomes null after the null check but before the invocation. 在空检查之后但在调用之前, SomethingHappened变为null可能性很小。 However, MulticastDelagate s are immutable, so if you first assign a variable, null check against the variable and invoke through it, you are safe from that scenario (self plug: I wrote a blog post about this a while ago). 但是, MulticastDelagate s是不可变的,因此,如果您首先分配一个变量,对该变量进行null检查并通过它进行调用,那么在这种情况下是安全的(自助插件:我前段时间写了一篇博客文章 )。

There is a back side of the coin though; 但是硬币的背面。 if you use the temp variable approach, your code is protected against NullReferenceException s, but it could be that the event will invoke event listeners after they have been detached from the event . 如果您使用temp变量方法,那么您的代码将受到NullReferenceException的保护,但可能是该事件将在事件监听器与事件分离后调用事件监听器。 That is just something to deal with in the most graceful way possible. 这只是以最优雅的方式处理的事情。

In order to get around this I have an extension method that I sometimes use: 为了解决这个问题,我有一个扩展方法,我有时会使用:

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

Using that method, you can invoke the events like this: 使用该方法,您可以像这样调用事件:

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

Since C# 6.0 you can use monadic Null-conditional operator ?. 从C#6.0开始,您可以使用Monadic Null条件运算符?. to check for null and raise events in easy and thread-safe way. 以简单且线程安全的方式检查null并引发事件。

SomethingHappened?.Invoke(this, args);

It's thread-safe because it evaluates the left-hand side only once, and keeps it in a temporary variable. 它是线程安全的,因为它只对左侧求值一次,并将其保留在一个临时变量中。 You can read more here in part titled Null-conditional operators. 您可以在标题为空条件运算符的部分中了解更多信息

Update: Actually Update 2 for Visual Studio 2015 now contains refactoring to simplify delegate invocations that will end up with exactly this type of notation. 更新:实际上,Visual Studio 2015的更新2现在包含重构以简化委托调用,而委托最终将以这种类型的表示法结束。 You can read about it in this announcement . 您可以在本公告中阅读有关内容。

I keep this snippet around as a reference for safe multithreaded event access for both setting and firing: 我保留此代码段作为设置和触发的安全多线程事件访问的参考:

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

For .NET 4.5 it's better to use Volatile.Read to assign a temp variable. 对于.NET 4.5,最好使用Volatile.Read分配一个临时变量。

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

Update: 更新:

It's explained in this article: http://msdn.microsoft.com/en-us/magazine/jj883956.aspx . 本文对此进行了解释: http : //msdn.microsoft.com/zh-cn/magazine/jj883956.aspx Also, it was explained in Fourth edition of "CLR via C#". 另外,在“通过C#进行CLR”的第四版中对此进行了说明。

Main idea is that JIT compiler can optimize your code and remove the local temporary variable. 主要思想是JIT编译器可以优化您的代码并删除本地临时变量。 So this code: 所以这段代码:

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

will be compiled into this: 将被编译为:

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

This happens in certain special circumstances, however it can happen. 在某些特殊情况下会发生这种情况,但是可能会发生。

Declare your event like this to get thread safety: 像这样声明您的事件以确保线程安全:

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

And invoke it like this: 并像这样调用它:

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

Although the method is not needed anymore.. 尽管不再需要该方法。

It depends on what you mean by thread-safe. 这取决于您所说的线程安全性。 If your definition only includes the prevention of the NullReferenceException then the first example is more safe. 如果您的定义仅包含NullReferenceException的预防措施,则第一个示例更为安全。 However, if you go with a more strict definition in which the event handlers must be invoked if they exist then neither is safe. 但是,如果使用更严格的定义,如果事件处理程序存在,则必须在其中调用事件处理程序, 因此 两者都不安全。 The reason has to do with the complexities of the memory model and barriers. 原因与内存模型和障碍的复杂性有关。 It could be that there are, in fact, event handlers chained to the delegate, but the thread always reads the reference as null. 实际上,可能有链接到委托的事件处理程序,但是线程始终将引用读取为null。 The correct way of fixing both is to create an explicit memory barrier at the point the delegate reference is captured into a local variable. 解决这两种错误的正确方法是在将委托引用捕获到局部变量的位置创建一个显式的内存屏障。 There are several ways of doing this. 有几种方法可以做到这一点。

  • Use the lock keyword (or any synchronization mechanism). 使用lock关键字(或任何同步机制)。
  • Use the volatile keyword on the event variable. 在事件变量上使用volatile关键字。
  • Use Thread.MemoryBarrier . 使用Thread.MemoryBarrier

Despite the awkward scoping problem which prevents you from doing the one-line initializer I still prefer the lock method. 尽管难以解决的范围问题使您无法执行单行初始化程序,但我仍然更喜欢使用lock方法。

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

It is important to note that in this specific case the memory barrier problem is probably moot because it is unlikely that reads of variables will be lifted outside method calls. 重要的是要注意,在这种特定情况下,内存屏障问题可能是没有实际意义的,因为不太可能将变量的读取移出方法调用之外。 But, there is no guarentee especially if the compiler decides to inline the method. 但是,没有保证,特别是如果编译器决定内联该方法。

Actually, the first is thread-safe, but the second isn't. 实际上,第一个是线程安全的,但是第二个不是。 The problem with the second is that the SomethingHappened delegate could be changed to null between the null verification and the invocation. 第二个问题是,在null验证和调用之间,可以将SomethingHappened委托更改为null。 For a more complete explanation, see http://blogs.msdn.com/b/ericlippert/archive/2009/04/29/events-and-races.aspx . 有关更完整的说明,请参见http://blogs.msdn.com/b/ericlippert/archive/2009/04/29/events-and-races.aspx

I tried to pimp out Jesse C. Slicer 's answer with: 我试图用以下方法来拉开杰西·C·斯莱特的答案:

  • Ability to sub/unsubscribe from any thread while within a raise (race condition removed) 能够在加薪期间取消/取消订阅任何线程(取消竞赛条件)
  • Operator overloads for += and -= at the class level 在类级别上+ =和-=的运算符重载
  • Generic caller defined delegates 通用调用方定义的委托

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

Usage: 用法:

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

Any major problems with this approach? 这种方法有什么重大问题吗?

The code was only briefly tested and edited a bit on insert. 仅在插入时对代码进行了简短的测试和编辑。
Pre-acknowledge that List<> not a great choice if many elements. 如果元素很多,请预先确认List <>不是一个很好的选择。

Actually, no, the second example isn't considered thread-safe. 实际上,不,第二个示例不被认为是线程安全的。 The SomethingHappened event could evaluate to non-null in the conditional, then be null when invoked. SomethingHappened事件在条件中可以评估为非null,然后在调用时为null。 It's a classic race condition. 这是经典的比赛条件。

为了使它们中的任何一个都是线程安全的,您假定所有订阅该事件的对象也是线程安全的。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM