C#事件和线程安全

UPDATE

从C#6开始,这个问题的答案是:

SomeEvent?.Invoke(this, e);

我经常听到/阅读以下建议:

在检查它为null并且启动之前,始终制作一个事件的副本。 这将消除线程潜在的问题,在检查null和发生事件的位置之间的位置,事件变为null

// Copy the event delegate before checking/calling
EventHandler copy = TheEvent;

if (copy != null)
    copy(this, EventArgs.Empty); // Call any handlers on the copied list

更新:我认为从阅读优化这可能也需要事件成员变化,但Jon Skeet在他的答案中指出,CLR不会优化副本。

但同时,为了使这个问题发生,另一个线程必须做到这样:

// Better delist from event - don't want our handler called from now on:
otherObject.TheEvent -= OnTheEvent;
// Good, now we can be certain that OnTheEvent will not run...

实际的顺序可能是这种混合:

// Copy the event delegate before checking/calling
EventHandler copy = TheEvent;

// Better delist from event - don't want our handler called from now on:
otherObject.TheEvent -= OnTheEvent;    
// Good, now we can be certain that OnTheEvent will not run...

if (copy != null)
    copy(this, EventArgs.Empty); // Call any handlers on the copied list

关键是OnTheEvent在作者取消订阅之后运行,但他们只是退订以避免发生这种情况。 当然,真正需要的是在addremove访问器中进行适当同步的自定义事件实现。 此外,如果在事件触发时进行锁定,则存在可能的死锁问题。

那么这个货物崇拜编程呢? 看起来是这样 - 很多人必须采取这一步骤,以防止多个线程,它们的代码时,它实际上在我看来,事件需要比这更多的照顾,他们可以被用作多线程设计的一部分之前。 因此,那些没有采取额外关怀的人可能会忽视这一建议 - 对单线程程序来说这不是问题,事实上,鉴于大多数在线示例代码中没有volatile ,建议可能会有根本没有效果。

(在成员声明中分配空的delegate { }并不是一件简单的事情,所以你从不需要首先检查null ?)

更新:如果不明确,我确实掌握了建议的意图 - 在所有情况下避免空引用异常。 我的观点是,这个特殊的空引用异常只能出现如果另一个线程从事件退市,以及这样做的唯一原因是为了确保没有进一步的调用将通过该事件,这显然不是通过这种技术实现了接收。 你会隐瞒一个竞争条件 - 最好揭示它! 该空例外有助于检测对您的组件的滥用情况。 如果你希望你的组件免受滥用,你可以按照WPF的例子 - 在你的构造函数中存储线程ID,然后在另一个线程试图直接与你的组件交互时抛出一个异常。 或者实现一个真正的线程安全组件(不是一件容易的事)。

所以我争辩说,仅仅做这个复制/检查习惯用法就是货物崇拜编程,给你的代码添加混乱和噪音。 要实际防止其他线程需要更多的工作。

更新回复Eric Lippert的博客文章:

所以这是我已经错过了有关事件处理的主要事情:“事件处理程序必须是在被称为甚至在事件发生后已取消订阅面对强大的”,显然因此我们只需要关心事件的可能性委托为null 。 对事件处理程序的要求是否在任何地方记录?

所以:“还有其他方法可以解决这个问题;例如,初始化处理程序以使其有一个永远不会被删除的空操作,但是执行空检查是标准模式。”

因此,我的问题的剩余部分是,为什么显式空检查“标准模式”? 另一种方法是分配空的委托,只需要将= delegate {}添加到事件声明中,这样就可以避免每一个事件发生的地方都有那么多的臭味仪式。 很容易确保空委托实例化便宜。 还是我仍然想念一些东西?

肯定肯定是这样的(正如Jon Skeet所说),这只是.NET 1.x的建议,并没有消失,因为它应该在2005年完成。


由于条件,JIT不允许在第一部分中进行优化。 我知道这是前段时间作为幽灵而提出的,但它是无效的。 (我前一段时间用Joe Duffy或Vance Morrison进行了检查;我记不起哪个。)

如果没有volatile修饰符,可能会导致本地副本过期,但仅此而已。 它不会导致NullReferenceException

是的,肯定有一个竞赛条件 - 但总会有的。 假设我们只是将代码更改为:

TheEvent(this, EventArgs.Empty);

现在假设该委托的调用列表有1000个条目。 在另一个线程退出接近列表末尾的处理程序之前,清单开始处的动作完全可能会被执行。 但是,该处理程序仍将被执行,因为它将成为一个新列表。 (代表是不可改变的。)据我所知,这是不可避免的。

使用空的委托肯定会避免无效性检查,但不会解决竞态条件。 它也不能保证你总是“看到”变量的最新值。


我看到很多人朝着这样做的扩展方法前进......

public static class Extensions   
{   
  public static void Raise<T>(this EventHandler<T> handler, 
    object sender, T args) where T : EventArgs   
  {   
    if (handler != null) handler(sender, args);   
  }   
}

这给你更好的语法来提高事件...

MyEvent.Raise( this, new MyEventArgs() );

并且在方法调用时捕获本地副本,因为它是在方法调用时捕获的。


“为什么显式 - 空检查'标准模式'?”

我怀疑这可能是因为空检查更高效。

如果您在创建事件时始终订阅空白委托,那么会有一些开销:

  • 构建空白委托的成本。
  • 构建委托链来包含它的代价。
  • 每次提出事件时调用无意义委托的代价。
  • (请注意,UI控件通常具有大量的事件,其中大多数事件从未订阅过。必须为每个事件创建一个虚拟用户然后调用它才可能导致重大性能下降。)

    我做了一些粗略的性能测试,以了解subscribe-empty-delegate方法的影响,以下是我的结果:

    Executing 50000000 iterations . . .
    OnNonThreadSafeEvent took:      432ms
    OnClassicNullCheckedEvent took: 490ms
    OnPreInitializedEvent took:     614ms <--
    Subscribing an empty delegate to each event . . .
    Executing 50000000 iterations . . .
    OnNonThreadSafeEvent took:      674ms
    OnClassicNullCheckedEvent took: 674ms
    OnPreInitializedEvent took:     2041ms <--
    Subscribing another empty delegate to each event . . .
    Executing 50000000 iterations . . .
    OnNonThreadSafeEvent took:      2011ms
    OnClassicNullCheckedEvent took: 2061ms
    OnPreInitializedEvent took:     2246ms <--
    Done
    

    请注意,对于零个或一个订阅者(通常用于UI控件,其中事件丰富)的情况,事先使用空委托进行初始化的事件显着较慢(超过5000万次迭代...)

    有关更多信息和源代码,请访问此问题(!)之前发布的.NET事件调用线程安全性的博客文章。

    (我的测试设置可能有缺陷,请随时下载源代码并自行检查,任何反馈都将非常感谢。)

    链接地址: http://www.djcxy.com/p/51469.html

    上一篇: C# Events and Thread Safety

    下一篇: Can using lambdas as event handlers cause a memory leak?