一尘不染

C#事件和线程安全

c#

更新

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

SomeEvent?.Invoke(this, e);

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

在检查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?)

更新: 如果不清楚,我确实掌握了建议的意图-
避免在所有情况下都出现空引用异常。我的观点是,仅当另一个线程从该事件中退出时,才可能发生此特定的null引用异常,而这样做的唯一原因是确保不会通过该事件接收到进一步的调用,而这种技术显然无法实现。您可能会隐藏种族状况-
最好公开一下!空异常有助于检测组件的滥用情况。如果希望保护组件免受滥用,则可以遵循WPF的示例-
将线程ID存储在构造函数中,如果另一个线程试图直接与您的组件进行交互,则抛出异常。否则,实现一个真正的线程安全组件(这不是一件容易的事)。

因此,我认为仅执行此复制/检查惯用语便是一种狂热的编程,给您的代码增加了混乱和噪音。要真正保护自己免受其他线程的攻击,需要进行大量工作。

更新以回应Eric Lippert的博客文章:

因此,关于事件处理程序,我错过了一件主要的事情:“即使在取消订阅事件之后,事件处理程序也必须在被调用时保持健壮”,因此,显然,我们只需要关心事件的可能性代表被null
对事件处理程序的要求是否记录在任何地方?

这样:“还有其他方法可以解决此问题;例如,初始化处理程序以使其具有从未删除的空动作。但是,执行空检查是标准模式。”

因此,我的问题剩下的一个片段是, 为什么要显式-空检查“标准模式”? 另一种方法是分配空的委托人,只需= delegate {}要将其添加到事件声明中,这样就消除了在引发事件的每个位置上堆积的臭臭仪式。确保空委托的实例化很容易,这很容易。还是我还缺少什么?

当然一定是(正如Jon Skeet所建议的那样),这仅仅是.NET 1.x的建议还没有像2005年那样被淘汰?


阅读 378

收藏
2020-05-19

共1个答案

一尘不染

由于条件的原因,不允许JIT执行您在第一部分中讨论的优化。我知道这是在不久前提出来的,但这是无效的。(前一段时间我曾与Joe Duffy或Vance
Morrison进行过核对;我不记得是哪个。)

如果没有volatile修饰符,则所获取的本地副本可能会过时,仅此而已。它不会导致NullReferenceException

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

TheEvent(this, EventArgs.Empty);

现在假设该委托的调用列表有1000个条目。在另一个线程取消订阅列表末尾的处理程序之前,很有可能在列表开头的操作已经执行。但是,该处理程序将仍然执行,因为它将是一个新列表。(代表们是一成不变的。)据我所知,这是不可避免的。

使用空委托当然可以避免无效检查,但不能解决竞争条件。它还不能保证您始终“看到”变量的最新值。

2020-05-19