.NET:EventHandler竞争条件修复如何工作?

时间:2011-02-03 23:05:55

标签: c# .net delegates

有以下模式用于在引发事件时避免竞争条件,以防另一个线程从MyEvent取消订阅,使其为空。

class MyClass
{
    public event EventHandler MyEvent;

    public void F()
    {
        EventHandler handler = MyEvent;
        if(handler != null)
            handler(this, EventArgs.Empty);
    }
}

与错误的做法相反,而这种做法很容易发生这种竞争:

class MyClass
{
    public event EventHandler MyEvent;

    public void F()
    {
        if(MyEvent != null)
            MyEvent(this, EventArgs.Empty);
    }
}

我的问题是,鉴于System.Delegate是引用类型:如果MyEvent不为null,为什么会这样?

EventHandler handler = MyEvent;

似乎复制其调用列表,而不是获取引用。

我希望将MyEvent委托分配给'handler'变量,然后一旦某人更改了 MyEvent,那么'handler'引用的对象也会被更改。

显然,事实并非如此,否则这个漂亮的小模式将无效。

我查看了.NET源代码但仍然无法在那里找到我的答案(它可能在那里,但我已经找了大约一个小时但找不到它,所以我在这里。) 我还阅读了C#语言规范对事件和代表的看法,但它没有解决这个问题。

感谢您的时间。

3 个答案:

答案 0 :(得分:9)

  一旦我得到了,我会期待的   '处理程序'中的MyEvent委托   参考,一旦有人改变   MyEvent那个'处理程序'的对象   引用也将被更改。   [..]   请注意,System.Delegate是一个类而不是结构。

虽然委托类型是引用类型是正确的,但它们是不可变引用类型。来自System.Delegate

  

“代表是不可变的; 一次   创建,一个的调用列表   代表不会改变。 [...]   组合操作,例如Combine   和删除,不要改变现有的   与会代表。相反,这样的   operation返回一个新的委托   包含操作的结果,   一个没有变化的代表,或者没什么。


另一方面,此模式解决的唯一问题是阻止尝试调用null委托引用。尽管有“修复”,但事件仍为prone to races

答案 1 :(得分:6)

更新

以下是一些图表,希望能够清除复制引用和赋值的混淆。

首先:复制参考。

x = y

在上图中,y中包含的参考被复制到x。没有人说复制对象;请注意 - 他们指向同一个对象。

第二:为变量分配新引用。

y += "!"

暂时忘掉+=运营商;我要强调的是,y正在为对象分配不同的引用。这不会影响x,因为x是它自己的变量。请记住,只有引用(图中的“地址”)已复制到y

第三:同样的事情,只有x

x += "?"

以上图表描述了string个对象,只是因为它们很容易以图形方式表示。但是对于委托来说却是一样的(记住,标准事件只是委托字段的包装)。您可以通过将<{1}}中的引用复制到上面的y进行查看,我们创建了一个不会受x后续作业影响的变量。< / p>

这是我们都熟悉的标准y种族条件“修复”背后的整个想法。


原始答案

你可能会对这个棘手的小语法感到困惑:

EventHandler

重要的是要知道,作为Ani points out in his answer,委托是不可变的引用类型(想想:就像someObject.SomeEvent += SomeEventHandler; )。许多开发人员错误地认为它们是可变的,因为上面的代码看起来像是在为一些可变列表“添加”处理程序。事实并非如此; string运算符是赋值运算符:它接受+=运算符的返回值,并将其赋值给左侧的变量。

(想想:+是不可变的,但我可以int做对吗?这是同样的事情。)


编辑:好的,从技术上讲这不太对。这是真正发生的事情。 int x = 0; x += 1;实际上是一个包装围绕一个委托字段,event+=只能 <(外部代码)访问运算符,分别编译为调用-=add。通过这种方式,它非常像一个属性,它(通常)是字段的包装器,其中访问属性并调用remove被编译为对=的调用和get

但重点仍然是:当您编写set时,被调用的+=方法会在内部将对新对象的引用分配给内部委托字段。我为在最初的答案中过度简化这个解释而道歉;但理解的关键原则是一样的。

顺便说一句,我涵盖自定义事件,您可以将自己的逻辑放在addadd方法中。这个答案仅适用于“正常”案例。


换句话说,当你这样做时......

remove

...您确实正在将引用复制到变量中。现在该引用位于局部变量中,并且本身不会被修改。如果它在赋值时指向实际对象,那么它将继续指向下一行上的相同(不可变)对象。如果它指向一个对象(EventHandler handler = SomeEvent; if (handler != null) { handler(this, EventArgs.Empty); } ),那么它仍然不会指向下一行的对象。

因此,如果其他地方的代码使用null订阅或取消订阅该事件,那么它真正做的是将原始引用更改为指向一个全新的对象。旧的委托对象仍然存在,你有一个对它的引用:在你的本地变量中。

答案 2 :(得分:1)

我想指出,将此事件与'int'案例进行比较可能本质上是错误的,因为即使'int'是原子的,它也是一种值类型。

但我认为我们已经解决了这个问题:

  

组合操作,例如Combine   和删除,不要改变现有的   与会代表。相反,这样的操作   返回包含的新委托   操作的结果,   不变的委托,或null。一个   组合操作返回null时   操作的结果是   没有参考的代表   至少一种方法。结合   操作返回不变   在请求的操作时委托   没有效果。

Delegate.CombineImpl Method 显示了实施。

我查看了.NET 4源代码中Delegate和MulticastDelegate的实现。 它们都没有声明+ =或 - =运算符。想到它,在Visual Basic.NET中你甚至没有它们,你使用AddHandler等...

这意味着C#编译器实现了这个功能,并且该类型与定义专用运算符实际上没有任何关系。

因此,当我这样做时,这会导致我得出一个合乎逻辑的结论:

EventHandler handler = MyEvent;

C#编译器将其转换为

EventHandler handler = EventHandler.Combine(MyEvent)

由于你的帮助,我很惊讶这个问题很快就解决了。

非常感谢你!