在检查事件是否为
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
< p > 更新:我从阅读有关优化的文章中认为,这也可能需要将事件成员设置为volatile,但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
访问器中进行适当的同步。此外,如果在触发事件时保持锁定,则可能会出现死锁问题。
那么这是Cargo Cult Programming吗?看起来是这样 - 很多人必须采取这一步骤来保护他们的代码免受多个线程的影响,但实际上,在将事件用作多线程设计的一部分之前,它们需要更加小心谨慎。因此,不注意这些额外细节的人可以忽略这个建议 - 这对于单线程程序根本不是问题,事实上,考虑到大多数在线示例代码中缺少volatile
,该建议可能根本没有任何效果。
(而且,只需在成员声明上分配空的delegate { }
就可以简单地避免在第一次检查null
的情况下检查)。
更新:如果不清楚,我已经理解了建议的意图——在任何情况下都要避免空引用异常。我的观点是这种特定的空引用异常只有在另一个线程从事件中注销时才会发生,而这样做的唯一原因是确保不会通过该事件接收到进一步的调用,而显然这种技术并没有实现这一点。你将掩盖竞态条件——揭示它会更好!那个空异常有助于检测组件的滥用。如果你想保护你的组件免受滥用,你可以遵循WPF的例子——在构造函数中存储线程ID,然后如果另一个线程试图直接与你的组件交互,抛出异常。或者实现一个真正的线程安全组件(这不是一项容易的任务)。
因此,我认为仅仅做这个复制/检查习惯是盲目模仿编程,会给你的代码增加混乱和噪音。要真正保护其他线程免受影响,需要更多的工作。
回应Eric Lippert博客文章的更新:
所以关于事件处理程序我错过了一个重要的事情:"即使在事件取消订阅后被调用,事件处理程序也必须具有强大的鲁棒性",因此我们只需要关注事件委托可能为null
的情况。 这个事件处理程序的要求在哪里记录?
所以:"解决这个问题还有其他方法;例如,将处理程序初始化为具有永远不会被删除的空操作。但进行空检查是标准模式。"
所以我问题的唯一剩余部分是,为什么显式空检查是“标准模式”?另一种选择是分配空委托,只需要在事件声明中添加= delegate {},就可以消除在每个引发事件的地方都存在的那些小臭堆。很容易确保空委托的实例化成本很低。或者我还是遗漏了什么吗?肯定是(正如Jon Skeet所建议的那样),这只是.NET 1.x的建议,它应该在2005年就已经消失了,对吧?更新
截至C# 6,这个问题的答案是:
SomeEvent?.Invoke(this, e);
EventName(arguments)
无条件地调用事件的委托,而不是只在委托非空时调用它(如果为空则不执行任何操作)。 - supercat