方法内联优化会导致竞争条件吗?

时间:2023-02-09 10:07:24

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

正如这个问题所示:使用扩展方法提升C#事件 - 是不是很糟糕?

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:

但是Mike Rosenblum在Jon Skeet的回答中提出了这个问题:

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.

你们需要将[MethodImpl(MethodImplOptions.NoInlining)]属性添加到这些扩展方法中,否则你可以通过JITter优化将代理复制到临时变量的尝试,从而允许空引用异常。

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:

我在发布模式下做了一些测试,看看当扩展方法没有标记为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.

我在发布模式下运行测试一段时间,从未有过NullReferenceException。

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

那么,Mike Rosenblum在他的评论中是错误的并且方法内联不能引起竞争条件吗?

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

事实上,我想真正的问题是,SaifeRaise会被描述为:

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

or

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

3 个解决方案

#1


7  

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.

问题不在于内联方法 - 无论是否内联,JITter都会通过内存访问来做有趣的事情。

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.

但是,我不认为这首先是一个问题。几年前它被提出作为一个问题,但我认为这被认为是对记忆模型的一个有缺陷的解读。只有一个逻辑“读取”变量,并且JITter无法对其进行优化,使得值在副本的一次读取和副本的第二次读取之间发生变化。

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.

现在,线程2是否更改myEvent的值无关紧要 - 它无法更改handler的值,因此您不会获得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.

如果JIT内联SafeRaise,它将被内联到此片段 - 因为内联参数有效地作为新的局部变量结束。问题只会是JIT错误地通过保留两个单独的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.

现在,至于为什么你只看到这种情况发生在调试模式:我怀疑附加调试器,线程相互中断的空间更大。可能还发生了一些其他优化 - 但它没有引入任何破损,所以没关系。

#2


5  

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).

在CLR规范中,没有什么能阻止优化器执行此操作。优化不会破坏单线程语义,并且只保证为易失性字段保留内存访问模式(即使这是一个不是100%真实的简化)。

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).

但是,Microsoft .NET框架记录了不同的内存模型。在该模型中,不允许优化器引入读取,并且您的代码是安全的(独立于内联优化)。

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.

也就是说,使用[MethodImplOptions]似乎是一个奇怪的黑客,因为阻止优化器引入读取只是不内联的副作用。我会使用volatile字段或Thread.VolatileRead。

#3


1  

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.

使用正确的代码,优化不应该改变其语义。因此,如果错误不在代码中,优化器不会引入错误。

#1


7  

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.

问题不在于内联方法 - 无论是否内联,JITter都会通过内存访问来做有趣的事情。

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.

但是,我不认为这首先是一个问题。几年前它被提出作为一个问题,但我认为这被认为是对记忆模型的一个有缺陷的解读。只有一个逻辑“读取”变量,并且JITter无法对其进行优化,使得值在副本的一次读取和副本的第二次读取之间发生变化。

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.

现在,线程2是否更改myEvent的值无关紧要 - 它无法更改handler的值,因此您不会获得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.

如果JIT内联SafeRaise,它将被内联到此片段 - 因为内联参数有效地作为新的局部变量结束。问题只会是JIT错误地通过保留两个单独的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.

现在,至于为什么你只看到这种情况发生在调试模式:我怀疑附加调试器,线程相互中断的空间更大。可能还发生了一些其他优化 - 但它没有引入任何破损,所以没关系。

#2


5  

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).

在CLR规范中,没有什么能阻止优化器执行此操作。优化不会破坏单线程语义,并且只保证为易失性字段保留内存访问模式(即使这是一个不是100%真实的简化)。

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).

但是,Microsoft .NET框架记录了不同的内存模型。在该模型中,不允许优化器引入读取,并且您的代码是安全的(独立于内联优化)。

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.

也就是说,使用[MethodImplOptions]似乎是一个奇怪的黑客,因为阻止优化器引入读取只是不内联的副作用。我会使用volatile字段或Thread.VolatileRead。

#3


1  

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.

使用正确的代码,优化不应该改变其语义。因此,如果错误不在代码中,优化器不会引入错误。