Can method inlining optimization cause race conditions?

后端 未结 3 900
感动是毒
感动是毒 2020-12-30 04:19

As seen in this question: Raising C# events with an extension method - is it bad?

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



        
相关标签:
3条回答
  • 2020-12-30 04:36

    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.

    0 讨论(0)
  • 2020-12-30 04:51

    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.

    0 讨论(0)
  • 2020-12-30 04:57

    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.

    0 讨论(0)
提交回复
热议问题