.NET: How does the EventHandler race-condition fix

2019-03-18 23:20发布

There's the following pattern which is used to avoid a race condition when raising events in case another thread unsubscribes from MyEvent, making it null.

class MyClass
{
    public event EventHandler MyEvent;

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

as opposed to the wrong way of doing it which is prone to this race condition:

class MyClass
{
    public event EventHandler MyEvent;

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

My question is, given that System.Delegate is a reference type: in case MyEvent is not null, how come

EventHandler handler = MyEvent;

seems to copy its invocation list instead of obtaining the reference.

I would expect that having the MyEvent delegate assigned to the 'handler' variable, then once somebody changed MyEvent that the object that 'handler' references would be changed as well.

Obviously, that is not the case, otherwise this nifty little pattern wouldn't work.

I've looked into the .NET source code and still could not find my answer there (it's probably there, but I've looked for about an hour and couldn't find it, so here I am.) I've also read what the C# Language Specification has to say about events and delegates, but it doesn't address this matter.

Thanks for your time.

3条回答
劳资没心,怎么记你
2楼-- · 2019-03-18 23:53

I would expect that once I got the MyEvent delegate inside the 'handler' reference, once somebody would change MyEvent that the object that 'handler' references will be changed as well. [..] Notice that System.Delegate is a class and not a struct.

Although you are correct that delegate-types are references-types, they are immutable reference-types. From System.Delegate:

"Delegates are immutable; once created, the invocation list of a delegate does not change.[...] Combining operations, such as Combine and Remove, do not alter existing delegates. Instead, such an operation returns a new delegate that contains the results of the operation, an unchanged delegate, or Nothing.


On another note, the only issue this pattern addresses is preventing the attempted invocation of a null delegate-reference. Events are prone to races despite this "fix".

查看更多
Melony?
3楼-- · 2019-03-18 23:55

Update

Here are some diagrams that should hopefully clear up confusion about copying references and assignment.

First: copying a reference.

x = y

In the above diagram, the reference contained in y is copied into x. No one's saying the object is copied; mind you—they point to the same object.

Second: assigning a new reference to a variable.

y += "!"

Forget about the += operator for a moment; what I want to highlight above is that y is being assigned a different reference, to a new object. This does not affect x because x is its own variable. Remember, only the reference (the "address" in the diagram) had been copied to y.

Third: same thing, only to x.

x += "?"

The above diagrams depict string objects, only because those are easy to represent graphically. But it's the same thing with delegates (and remember, standard events are just wrappers around delegate fields). You can see how by copying the reference in y into x above, we have created a variable which will not be affected by subsequent assignments to y.

That is the whole idea behind the standard EventHandler race condition "fix" we're all familiar with.


Original Answer

You are probably confused by this tricky little syntax:

someObject.SomeEvent += SomeEventHandler;

What's important to realize is that, as Ani points out in his answer, delegates are immutable reference types (think: just like string). Many developers mistakenly think they are mutable because the above code looks like I am "adding" a handler to some mutable list. This isn't so; the += operator is an assignment operator: it takes the return value of the + operator and assigns it to the variable on the left side.

(Think: int is immutable, and yet I can do int x = 0; x += 1; right? It's the same thing.)


EDIT: OK, technically this is not quite right. Here is what really happens. An event is actually a wrapper around a delegate field that is accessible only (to external code) by the += and -= operators, which are compiled to calls to add and remove, respectively. In this way it is very much like a property, which is (typically) a wrapper around a field, where accessing the property and calling = are compiled to calls to get and set.

But the point still remains: when you write +=, the add method that gets called is internally assigning a reference to a new object to the internal delegate field. I apologize for oversimplifying this explanation in my initial answer; but the key principle to understand is the same.

By the way, I am not covering custom events where you can put your own logic inside the add and remove methods. This answer only applies to the "normal" case.


In other words, when you do this...

EventHandler handler = SomeEvent;
if (handler != null)
{
    handler(this, EventArgs.Empty);
}

...you are indeed copying a reference into a variable. Now that reference is in the local variable and won't itself be modified. If it was pointing to an actual object at the time of assignment, then it will continue to point at that same (immutable) object on the next line. If it was not pointing to an object (null), then it will still not point to an object on the next line.

So if code elsewhere subscribed or unsubscribed to the event using +=, what it really did was change the original reference to point to a completely new object. The old delegate object is still around, and you've got a reference to it: in your local variable.

查看更多
啃猪蹄的小仙女
4楼-- · 2019-03-18 23:57

I would like to point out that comparing this incident to the 'int' case is probably inherently wrong since even though 'int' is atomic, it is a value type.

But I think we've solved the case:

Combining operations, such as Combine and Remove, do not alter existing delegates. Instead, such an operation returns a new delegate that contains the results of the operation, an unchanged delegate, or null. A combining operation returns null when the result of the operation is a delegate that does not reference at least one method. A combining operation returns an unchanged delegate when the requested operation has no effect.

Delegate.CombineImpl Method shows the implementation.

I looked over the implementation of Delegate and MulticastDelegate in the .NET 4 source code. Neither of them declare the += or -= operator. Coming to think of it, in Visual Basic.NET you don't even have them, you use AddHandler, etc...

This means that the C# compiler implements this functionality and that the type doesn't really have anything to do with defining specialized operators.

So this leads me to a logical conclusion which is, when you do:

EventHandler handler = MyEvent;

the C# compiler translates it to

EventHandler handler = EventHandler.Combine(MyEvent)

I'm surprised how quickly this question was solved thanks to your help.

Thank you very much indeed!

查看更多
登录 后发表回答