更新
从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作者取消订阅之后,但是他们只是专门取消订阅以避免这种情况的发生。当然,真正需要的是在add和remove访问器中具有适当同步的自定义事件实现。此外,如果在触发事件时持有锁,则可能会出现死锁。
OnTheEvent
add
remove
那么这是《货运崇拜编程》吗?似乎是这样- 许多人必须采取这一步骤来保护自己的代码免受多个线程的侵害,而在我看来,实际上,在将事件用作多线程设计的一部分之前,事件需要比这多得多的关注。因此,那些没有特别注意的人也可能会忽略此建议- 对于单线程程序来说这根本不是问题,实际上,鉴于volatile大多数在线示例代码中都没有,该建议可能没有完全没有效果。
volatile
(而且delegate { }在成员声明中分配空值是否更简单,这样您就不必首先检查null?)
delegate { }
更新: 如果不清楚,我确实掌握了建议的意图- 避免在所有情况下都出现空引用异常。我的观点是,仅当另一个线程从该事件中退出时,才可能发生此特定的null引用异常,而这样做的唯一原因是确保不会通过该事件接收到进一步的调用,而这种技术显然无法实现。您可能会隐藏种族状况- 最好公开一下!空异常有助于检测组件的滥用情况。如果希望保护组件免受滥用,则可以遵循WPF的示例- 将线程ID存储在构造函数中,如果另一个线程试图直接与您的组件进行交互,则抛出异常。否则,实现一个真正的线程安全组件(这不是一件容易的事)。
因此,我认为仅执行此复制/检查惯用语便是一种狂热的编程,给您的代码增加了混乱和噪音。要真正保护自己免受其他线程的攻击,需要进行大量工作。
更新以回应Eric Lippert的博客文章:
因此,关于事件处理程序,我错过了一件主要的事情:“即使在取消订阅事件之后,事件处理程序也必须在被调用时保持健壮”,因此,显然,我们只需要关心事件的可能性代表被null。 对事件处理程序的要求是否记录在任何地方?
这样:“还有其他方法可以解决此问题;例如,初始化处理程序以使其具有从未删除的空动作。但是,执行空检查是标准模式。”
因此,我的问题剩下的一个片段是, 为什么要显式-空检查“标准模式”? 另一种方法是分配空的委托人,只需= delegate {}要将其添加到事件声明中,这样就消除了在引发事件的每个位置上堆积的臭臭仪式。确保空委托的实例化很容易,这很容易。还是我还缺少什么?
= delegate {}
当然一定是(正如Jon Skeet所建议的那样),这仅仅是.NET 1.x的建议还没有像2005年那样被淘汰?
由于条件的原因,不允许JIT执行您在第一部分中讨论的优化。我知道这是在不久前提出来的,但这是无效的。(前一段时间我曾与Joe Duffy或Vance Morrison进行过核对;我不记得是哪个。)
如果没有volatile修饰符,则所获取的本地副本可能会过时,仅此而已。它不会导致NullReferenceException。
NullReferenceException
是的,肯定有比赛条件-但总会存在。假设我们只是将代码更改为:
TheEvent(this, EventArgs.Empty);
现在假设该委托的调用列表有1000个条目。在另一个线程取消订阅列表末尾的处理程序之前,很有可能在列表开头的操作已经执行。但是,该处理程序将仍然执行,因为它将是一个新列表。(代表们是一成不变的。)据我所知,这是不可避免的。
使用空委托当然可以避免无效检查,但不能解决竞争条件。它还不能保证您始终“看到”变量的最新值。