事件处理程序不是线程安全的吗?

29

我读到过,不应该直接调用一个事件,而是使用

if (SomeEvent != null)
   SomeEvent(this, null);

我应该要做什么。

SomeEventHandler temp = SomeEvent;
if (temp != null)
    temp(this, null);

为什么会这样?第二个版本如何变得线程安全?最佳实践是什么?


阅读这里的初步、合格答案,我感觉C#中的事件处理是紧密耦合的、容易出错的,并且不是很理解。 - micahhoover
4个回答

32

在我看来,其他答案忽略了一个关键细节——委托(和因此而来的事件)是不可变的。这意味着订阅或取消订阅事件处理程序并不只是简单地添加/删除列表,而是用具有额外项(或少一项)的新列表替换它。

由于引用是原子性的,这意味着在执行以下操作时:

var handler = SomeEvent;

现在你拥有一个不可变的实例,即使在下一皮秒另一个线程取消订阅(导致实际事件字段变为null),它也无法更改。

因此,您需要测试null并调用它,一切都很好。当然,请注意仍然存在一个混乱的情况,即事件在认为一皮秒前已取消订阅的对象上被引发


这解决了一个问题,但是替换本身是否安全地进行,以确保所有请求的操作都发生了呢?也就是说,如果两个线程同时尝试订阅不同的委托,是否保证最终都会被订阅,还是其中一个订阅可能会悄悄失败? - binki
1
@binki 是的;它使用了一个交错交换循环(现代编译器),或者一个同步(lock)区域(旧版编译器)。 - Marc Gravell

14

事件实际上是一个委托列表的语法糖。当您调用事件时,它实际上是在迭代该列表并使用传递的参数调用每个委托。

线程的问题是它们可能会通过订阅/取消订阅来添加或删除此集合中的项。如果它们在您迭代集合时执行此操作,则会引发问题(我认为会抛出异常)。

意图是在迭代列表之前复制列表,这样您就可以受到对列表的更改的保护。

注意:现在可能会在取消订阅后仍调用您的侦听器,因此您应确保在侦听器代码中处理此情况。


3
在遍历集合时,实际上不会出现问题。这里使用的委托是不可变的。唯一的问题是在检查是否有人已订阅并实际调用事件处理程序之前的瞬间。一旦开始执行它们,任何后台更改都不会影响当前调用。 - Lasse V. Karlsen
补充Lasse V. Karlsen的评论,您可以使用SomeEvent?.Invoke(...)来代替在调用之前检查null,从而消除最后一块竞态条件。 - ultimA

5

最佳实践是使用第二种形式。原因是在“if”测试和调用之间,另一个线程可能会将SomeEvent设置为null或更改它。


为什么第二个语句不能发生? - Daniel
2
读取SomeEvent的操作是原子性的,即要么全部执行完毕,要么不执行。因此,由于temp是局部变量,它不能在该线程之外被修改。 - user7116

2
这里是一篇关于.NET事件和线程竞争条件的好文章(Here)。它涵盖了一些常见情况,并提供了一些很好的参考资料。
希望这可以帮到你。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接