views:

214

answers:

3

As seen in this question: http://stackoverflow.com/questions/231525/raising-c-events-with-an-extension-method-is-it-bad

I'm thinking of using this extension method to safely raise an event:

public static void SafeRaise(this EventHandler handler, object sender, EventArgs e)
{
    if (handler != null)
        handler(sender, e);
}

But Mike Rosenblum raise this concern in Jon Skeet's answer:

You guys need to add the [MethodImpl(MethodImplOptions.NoInlining)] attribute to these extension methods or else your attempt to copy the delegate to a temporary variable could be optimized away by the JITter, allowing for a null reference exception.

I did some test in Release mode to see if I could get a race condition when the extension method is not marked with NoInlining:

int n;
EventHandler myListener = (sender, e) => { n = 1; };
EventHandler myEvent = null;

Thread t1 = new Thread(() =>
{
    while (true)
    {
        //This could cause a NullReferenceException
        //In fact it will only cause an exception in:
        //    debug x86, debug x64 and release x86
        //why doesn't it throw in release x64?
        //if (myEvent != null)
        //    myEvent(null, EventArgs.Empty);

        myEvent.SafeRaise(null, EventArgs.Empty);
    }
});

Thread t2 = new Thread(() =>
{
    while (true)
    {
        myEvent += myListener;
        myEvent -= myListener;
    }
});

t1.Start();
t2.Start();

I ran the test for a while in Release mode and never had a NullReferenceException.

So, was Mike Rosenblum wrong in his comment and method inlining cannot cause race condition?

In fact, I guess the real question is, will SaifeRaise be inlined as:

while (true)
{
    EventHandler handler = myEvent;
    if (handler != null)
        handler(null, EventArgs.Empty);
}

or

while (true)
{
    if (myEvent != null)
        myEvent(null, EventArgs.Empty);
}
+1  A: 

With correct code, optimizations should not change its semantics. Therefore no error can be introduced by the optimizer, if the error was not in the code already.

Vlad
+1 In the big picture, this answer would actually make the most sense, above and beyond any of the specifics of this particular case. That said, there are exceptions, such as when using reflection code, e.g., walking the stack.
Mike Rosenblum
+4  A: 

The problem wouldn't have been inlining the method - it would have been the JITter doing interesting things with memory access whether or not it was inlined.

However, I don't believe it is an issue in the first place. It was raised as a concern a few years back, but I believe that was regarded as a flawed reading of the memory model. There's only one logical "read" of the variable, and the JITter can't optimise that away such that the value changes between one read of the copy and the second read of the copy.

EDIT: Just to clarify, I understand exactly why this is causing a problem for you. You've basically got two threads modifying the same variable (as they're using captured variables). It's perfectly possible for the code to occur like this:

Thread 1                      Thread 2

                              myEvent += myListener;

if (myEvent != null) // No, it's not null here...

                              myEvent -= myListener; // Now it's null!

myEvent(null, EventArgs.Empty); // Bang!

This is slightly less obvious in this code than normally, as the variable is a captured variable rather than a normal static/instance field. The same principle applies though.

The point of the safe raise approach is to store the reference in a local variable which can't be modified from any other threads:

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

Now it doesn't matter whether thread 2 changes the value of myEvent - it can't change the value of handler, so you won't get a NullReferenceException.

If the JIT does inline SafeRaise, it will be inlined to this snippet - because the inlined parameter ends up as a new local variable, effectively. The problem would only be if the JIT incorrectly inlined it by keeping two separate reads of myEvent.

Now, as to why you only saw this happen in debug mode: I suspect that with the debugger attached, there's far more room for threads to interrupt each other. Possibly some other optimisation occurred - but it didn't introduce any breakage, so that's okay.

Jon Skeet
Inlining might change the performance characteristics and exacerbate a pre-existing race condition, however.
Joel
Well, since I started this mess, I guess I should reply, eh? Jon, your explanation makes perfect sense. Unless the event handler were marked 'volatile', there would be no reason to fetch the value again after first checking it within the 'if' statement. This would be true whether the method were inlined or not. That said, my source for this was Juval Lowy in his book "Programming .NET Components", which was for .NET 2.0, but is still very relevant and is an outstanding read. Juval goes into this issue in detail, in particular on pp.250-252.
Mike Rosenblum
@Mike: I know there was some discussion of it around the time of .NET 2.0's release due to a buggy x64 JIT optimisation (fixed before release, I'm pretty sure). I suspect in this one case, Juval is incorrect. Even the best authors are allowed to make mistakes occasionally :)
Jon Skeet
@Jon, yes, I agree, it does seem that Juval may have been mistaken here, or was himself relying on another seemingly-reliable source. I'm not even close to being in his league, though, so I am hesitant to be conclusive. Event handling does not usually require high-speed responsiveness, so I don't mind including the 'MethodImplAttribute' here, although I do agree 100% with your reasoning and your conclusion.
Mike Rosenblum
Actually, if this line of reasoning is correct, why do we need to create a separate temporary variable at all? Unless the field holding the event handlers is marked as 'volatile', then isn't this unnecessary? So why the conventional wisdom? But if storing a copy of the event handlers to a local, temporary variable *is* necessary, then I think this brings us back to protecting against inlining being a strong consideration again. Lastly, Jeff Cyr's code shows that errors do occur in debug mode, but not in release mode, which is something that would seem worth understanding as well.
Mike Rosenblum
@Mike: The value of the *instance* variable can change between the test for nullity and the invocation, if another thread is busy doing stuff. The value of the *local* variable can't change - no other thread has access to it. Inlining has nothing to do with that - it would still be a local variable even after inlining, just local to a different method. As for the debug/release problem: diagnosing threading issues is tricky, and running under a debugger *radically* changes what's going to be happening in terms of timeslices etc.
Jon Skeet
@Jon: Ok, right, my bad: delegates are reference types, so the 'volatile' issue is not relevant. This brings us back to the JITer. Clearly debug vs. release will do very different things, agreed, but I think that relying on how the timeslices might differ is sketchy. I can't replicate Jeff Cyr's statement that can get an exception in debug mode. Even adding 'Thread.Sleep((new Random()).Next(0, 1000))' in several key locations cannot seem to cause an exception in either debug or release modes. So it does appear to be safe. I'd prefer have a definitive spec somewhere saying so, however.
Mike Rosenblum
@Mike Did you try with a dual core CPU? The race condition would be very rare with a single core CPU, but with a dual core I get the NullReferenceException in less than a second. With my code sample, when I use `if (myEvent != null) myEvent(null, EventArgs.Empty);`, in Debug it will throw the exception with x86 and x64 platforms, but in Release, it only throws with the x86 platform, weird eh?
SelflessCoder
@Jeff: Yes, to dual-core and, yes, definitely weird. But the fact that you have proven that one absolutely *can* have this problem come up is *very* worrisome. Omitting the 'MethodImplAttribute' goes against conventional wisdom and the cost of using it is almost zero cost. Therefore, someone should run this through CHESS, compiled against numerous platforms. Best would be to find where the JITer specification is explicit about this being safe -- but I think the JITer specification is relatively loose and its exact implementation can change in different versions. Until proven, keep using it.
Mike Rosenblum
@Jeff: Ah, ok, you snagged on this problem using x86 compiled in Release mode, but not in x64. Very interesting... I guess the JITer may have been updated to protect against this for x64? Just guessing, of course -- this might not hold in all cases and even x64 might be vulnerable in many circumstances. It would be great if the JITer's specification were explicit about this -- and explained the difference between x86 and x64. Until one can be *certain* of what is going on here, I think we all have no choice but to to continue to use the 'MethodImplAttribute'.
Mike Rosenblum
@Mike: I'm not sure we're talking about the same thing here, I'll try to clarify. When using the SafeRaise extension method, I never had a NullReferenceException. The exception is thrown(except with Release x64) when `if (myEvent != null) myEvent(null, EventArgs.Empty);` is uncommented at line 12 from my test bench code sample. So I don't think that the MethodImplAttribute is required on the extension method.
SelflessCoder
Yes, sorry, I did misunderstand. Gotcha now. So the problem exists (as expected) when you do not pass the event handler into a separate method. But the 'MethodImplAttribute' does not appear to be necessary, so I guess that the hypothesized inlining does not occur. But I wonder why not? Is the 'if' statement within the method sufficient to prevent inlining? Would such inlining be impossible in a future version of the JITer? My guess is that it's safe, but I'd still prefer to be sure, both out of curiosity and because the attribute is almost zero cost and guarantees 100% safety.
Mike Rosenblum
Again, this isn't a matter of inlining. It's a matter of whether the "copy to local variable, read from local, test, read from local" can be validly optimised to "read from field, test, read from field". I don't believe that's a valid optimisation.
Jon Skeet
@Jon: But Jeff comments that this basic optimization *does* occur in debug mode for both x86 and x64, and (of greater concern) in release mode for x86 - but not in release mode for x64. So they may have flagged this as a problematic optimization and have prevented this for x64, or this optimization no longer occurs for other reasons (i.e., this is pure luck). If this optimization *can* occur, then it is only the prevention of inlining that provides true protection. So we can either rely on faith that the JITer cannot inline in this case, or choose to *enforce* that it cannot via the attribute.
Mike Rosenblum
@Jeff: In the commented out code, `myEvent` is a captured variable which can be changed by other threads. It's *not* an independent local variable. I'll edit my answer to explain this. Again, it's nothing to do with inlining.
Jon Skeet
@Jon: I knew that. I'm just being curious and wondering why the race condition happen in Release x86 and never happen in Release x64. The x64 jitter must do something different that prevent the race condition to occur. Thanks for your time!
SelflessCoder
@Jeff: Sorry, that comment was meant to be @Mike, not you. Doh!
Jon Skeet
@Jon: Yes, I follow it all, but I'm not at your level, so I'm not in a position to know what can be optimized away and/or inlined. Fwiw, Lowy in his book *begins* by arguing (perhaps incorrectly) that `EventHandler handler = m_click; if (handler != null){handler(this, EventArgs.Empty)};` could have the temporary variable optimized away. He then progresses to creating a `RaiseEvent(EventHandler)` method, but then argues that inlining could bring us to the same place. He then explains that the `MethodImplAttribute` is the solution. I guess it is his initial assumption that was incorrect.
Mike Rosenblum
@Mike: Yes, the initial assumption is the problem here. In a normal situation (where we don't have captured variables) using a local variable is good enough.
Jon Skeet
@Jon: It is good enough on the Microsoft implementation of the .NET framework. But I couldn't find anything in the CLI specification that guarantees such optimizations cannot happen - such a guarantee is given only for volatile fields, not regular ones.
Daniel
@Daniel: I'll have a look, but I'd suggest that any such optimisation is completely bonkers. If you can't rely on the value of a *purely local variable* being unchanged between two reads, there's not a lot you can rely on.
Jon Skeet
+3  A: 

This is a memory model issue.

Basically the question is: if my code contains only one logical read, may the optimizer introduce another read?

Surprisingly, the answer is: maybe

In the CLR specification, nothing prevents optimizers from doing this. The optimization doesn't break single-threaded semantics, and memory access patterns are only guaranteed to be preserved for volatile fields (and even that's a simplication that's not 100% true).

Thus, no matter whether you use a local variable or a parameter, the code is not thread-safe.

However, the Microsoft .NET framework documents a different memory model. In that model, the optimizer is not allowed to introduce reads, and your code is safe (independent of the inlining optimization).

That said, using [MethodImplOptions] seems like a strange hack, as preventing the optimizer from introducing reads is only a side effect of not inlining. I'd use a volatile field or Thread.VolatileRead instead.

Daniel
Daniel, thanks for the clarification... I knew there must have been some hair-splitting going on here. But I don't see how using a volatile field would help here? We are already explicitly introducing a temporary variable to *prevent* the field from being read a second time. If we truly treat the field as volatile and recognize that writes from other threads are possible, then each null-check is immediately worthless.. We'd have to simply invoke the delegate without the null-check and swallow any NullReferenceException within a try-catch block (which is not a good idea).
Mike Rosenblum
You need a local variable AND a volatile field. A single read from a volatile field is guaranteed to read only a single time.Reads from normal fields have no such guarantee in the CLR specification (also known as: the optimizers may introduce reads and eliminate your local variable).But normal fields do have this guarantee in the MS memory model, so the difference is mostly theoretical (AFAIK even Mono implements the MS memory model).
Daniel
I see, *very* interesting, and subtle. Great info, thanks.
Mike Rosenblum