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

Yam*_*vic 11 .net c# delegates

如果另一个线程从MyEvent取消订阅,使其为null,则会出现以下模式,用于在引发事件时避免竞争条件.

class MyClass
{
    public event EventHandler MyEvent;

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

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

class MyClass
{
    public event EventHandler MyEvent;

    public void F()
    {
        if(MyEvent != null)
            MyEvent(this, EventArgs.Empty);
    }
}
Run Code Online (Sandbox Code Playgroud)

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

EventHandler handler = MyEvent;
Run Code Online (Sandbox Code Playgroud)

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

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

显然,情况并非如此,否则这个漂亮的小图案将无效.

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

谢谢你的时间.

Ani*_*Ani 9

我希望,一旦我在'handler'引用中获得MyEvent委托,一旦有人改变了MyEvent,那么'handler'引用的对象也将被更改.[..]请注意,System.Delegate是一个类而不是结构.

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

"代表是不可改变.一旦创建,委托的调用列表中不会改变.[...]组合操作,如合并和删除,不改变现有的委托相反,这样的操作返回一个包含一个新的委托操作的结果,一个未改变的代表,或没什么.


另一方面,此模式解决的唯一问题是阻止尝试调用null委托引用.尽管有这种"修复",但事件很容易发生.

  • 我想补充一点,当你将方法添加到调用列表时,你做`+ =`实际上取代了变量的值.这就是它看起来可变的原因. (2认同)

Dan*_*Tao 7

更新

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

第一:复制参考.

x = y

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

第二:为变量分配新的引用.

y + =

+=暂时忘掉操作员; 我要强调的是,y对象分配了不同的引用.这不会影响因为它是自己的变量.请记住,只有参考(图中的"地址")已被复制到.xxy

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

x + =

上图描绘了string对象,只是因为它们易于以图形方式表示.但是对于委托来说却是一样的(记住,标准事件只是委托字段的包装).你可以看到如何通过复制在基准yx上述情况,我们已经创建了不会被后续任务受到影响的变量y.

这就是EventHandler我们都熟悉的标准竞赛条件"修复" 背后的整个想法.


原始答案

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

someObject.SomeEvent += SomeEventHandler;
Run Code Online (Sandbox Code Playgroud)

重要的是要知道,正如Ani在他的回答中指出的那样,委托是不可变的引用类型(想想:就像string).许多开发人员错误地认为它们是可变的,因为上面的代码看起来像是在为一些可变列表"添加"处理程序.事实并非如此; 所述+=操作者是一个赋值运算符:它采用的返回值+算子并将其分配给左侧的变量.

(想想:int是不可改变的,但我可以做int x = 0; x += 1;对吗?这是一回事.)


编辑:好的,从技术上讲,这是不对的.这是真正发生的事情.一个event实际上是一个包装周围的委托字段是访问由(外部代码)+=-=运营商,这是编译调用addremove分别.通过这种方式,它非常像一个属性,它(通常)是一个字段的包装器,其中访问属性和调用=被编译为调用getset.

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

顺便说一句,我没有介绍自定义事件,你可以将自己的逻辑放在addremove方法中.这个答案仅适用于"正常"情况.


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

EventHandler handler = SomeEvent;
if (handler != null)
{
    handler(this, EventArgs.Empty);
}
Run Code Online (Sandbox Code Playgroud)

...你确实正在将引用复制到变量中.现在该引用位于局部变量中,并且本身不会被修改.如果它在赋值时指向实际对象,那么它将继续指向下一行上的相同(不可变)对象.如果它没有指向object(null),那么它仍然不会指向下一行的对象.

因此,如果其他地方的代码订阅或取消订阅事件使用+=,它真正做的是将原始引用更改为指向一个全新的对象.旧的委托对象仍然存在,并且您有一个对它的引用:在您的局部变量中.