【问题标题】:Checking for null before event dispatching... thread safe?在事件分派之前检查 null ......线程安全吗?
【发布时间】:2010-09-21 21:52:18
【问题描述】:

让我感到困惑,但从未引起任何问题的事情......推荐的事件调度方式如下:

public event EventHandler SomeEvent;
...
{
    ....
    if(SomeEvent!=null)SomeEvent();
}

在多线程环境中,这段代码如何保证在检查空值和调用事件之间,另一个线程不会改变SomeEvent的调用列表?

【问题讨论】:

    标签: c# multithreading events


    【解决方案1】:

    正如您所指出的,在多个线程可以同时访问SomeEvent 的情况下,一个线程可以检查SomeEvent 是否为空并确定它不是。就在这样做之后,另一个线程可以从SomeEvent 中删除最后一个注册的委托。当第一个线程尝试引发SomeEvent 时,将引发异常。避免这种情况的合理方法是:

    protected virtual void OnSomeEvent(EventArgs args) 
    {
        EventHandler ev = SomeEvent;
        if (ev != null) ev(this, args);
    }
    

    这是因为每当使用 add 和 remove 访问器的默认实现将委托添加到事件中或从事件中删除委托时,都会使用 Delegate.Combine 和 Delegate.Remove 静态方法。这些方法中的每一个都返回一个新的委托实例,而不是修改传递给它的那个。

    此外,.NET 中对象引用的赋值是atomic,添加和删除事件访问器的默认实现是synchronised。因此,上面的代码通过首先将多播委托从事件复制到临时变量来成功。在此之后对 SomeEvent 的任何更改都不会影响您制作和存储的副本。因此,您现在可以安全地测试是否有任何委托已注册并随后调用它们。

    请注意,此解决方案解决了一个竞争问题,即事件处理程序在被调用时为 null。它不处理事件处理程序在被调用时失效或事件处理程序在获取副本后订阅的问题。

    例如,如果事件处理程序依赖于在取消订阅处理程序后立即销毁的状态,则此解决方案可能会调用无法正常运行的代码。有关详细信息,请参阅Eric Lippert's excellent blog entry。另请参阅this StackOverflow question and answers

    编辑:如果您使用的是 C# 6.0,那么 Krzysztof's answer 看起来是个不错的选择。

    【讨论】:

    • 我对“此外,.NET 中对象引用的分配是线程安全的”语句有疑问。你肯定是说原子的?据我所知,如果线程 A 在变量 V 上设置了一个引用,则无法保证线程 B 会在变量 V 中设置更新后的引用,除非 V 是 volatile 或在读写 V 时使用了 lock 语句。
    • 这也意味着你的例子被打破了。如果线程 A 将事件处理程序添加到 SomeEvent,然后线程 B 调用 SomeEvent,那么很可能线程 B 将 SomeEvent 视为 null,除非 SomeEvent 被声明为 volatile
    • @Christophe,通过“线程安全”,我的意思是在一个线程中分配对象引用永远不会被另一个线程中断或视为不一致。正如我在更新中提到的,这与说线程 A 和线程 B 将始终具有相同的事件视图绝对不是一回事。这个例子所做的只是防止一个特定的竞争条件,而不是每个竞争条件。
    • @RoadWarrior,由于这个问题仍然有流量,我将最佳答案授予?.Invoke 答案,以便那些寻求快速解决方案的人首先看到它。
    • @spender,同意 - Krzysztof 的回答现在是最好的方法。
    【解决方案2】:

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

    SomeEvent?.Invoke(this, args);
    

    它是线程安全的,因为它只对左侧求值一次,并将其保存在一个临时变量中。您可以阅读更多 here 部分标题为 Null 条件运算符。

    【讨论】:

    • 当我们升级到 vs2015 并且我有机会使用新的语言功能时,我可能会回来并将其标记为正确答案。
    • 不错。这现在看起来像THE事实上的方法。
    【解决方案3】:

    删除此空检查的最简单方法是将事件处理程序分配给匿名委托。所产生的惩罚非常少,并且可以免除您所有的空检查、竞争条件等。

    public event EventHandler SomeEvent = delegate {};

    相关问题:Is there a downside to adding an anonymous empty delegate on event declaration?

    【讨论】:

    • 为什么这没有被标记为“官方”答案?这是唯一的好答案。
    • 我不同意。该方法似乎是一种 hack,不会使触发事件“安全”或可靠,它只能解决空值检查问题。请在相关线程中查看我的解释。
    【解决方案4】:

    推荐的方式有点不同,使用临时如下:

    EventHandler tmpEvent = SomeEvent;
    if (tmpEvent != null)
    {
        tmpEvent();
    }
    

    【讨论】:

    • 那会克隆调用列表吗?
    • 另外,您可以将 GetInvocationList 调用到一个临时的并按顺序执行每个。
    • dp,你不需要这样做。请参阅我的答案以了解原因。
    • RW:同意。我试图指出这基本上是做同样的事情。好答案。 +1
    【解决方案5】:

    更安全的方法:

    public class Test { private EventHandler myEvent; private object eventLock = new object(); private void OnMyEvent() { EventHandler handler; lock(this.eventLock) { handler = this.myEvent; } if (handler != null) { handler(this, EventArgs.Empty); } } public event MyEvent { add { lock(this.eventLock) { this.myEvent += value; } } remove { lock(this.eventLock) { this.myEvent -= value; } } } }

    -账单

    【讨论】:

    • 这是 Jon Skeet 在yoda.arachsys.com/csharp/events.html 中给出的正确方法
    • 您已将处理程序的分配与锁同步,但在离开同步块后不会检查 null 仍然允许相同的问题?
    【解决方案6】:

    我想建议对 RoadWarrior 的回答稍作改进 利用 EventHandler 的扩展函数:

    public static class Extensions
    {
        public static void Raise(this EventHandler e, object sender, EventArgs args = null)
        {
            var e1 = e;
    
            if (e1 != null)
            {
                if (args == null)
                    args = new EventArgs();
    
                e1(sender, args);
            }                
        }
      }
    

    有了这个范围内的扩展,事件可以简单地通过:

    类 SomeClass { 公共事件 EventHandler MyEvent;

    void SomeFunction()
    {
        // code ...
    
        //---------------------------
        MyEvent.Raise(this);
        //---------------------------
    }
    

    }

    【讨论】:

      猜你喜欢
      • 2013-08-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-02-14
      • 2010-11-05
      • 1970-01-01
      相关资源
      最近更新 更多