方法内联优化能否导致竞争条件?

时间:2010-02-24 15:53:18

标签: .net race-condition inline-method

如此问题所示: Raising C# events with an extension method - is it bad?

我正在考虑使用此扩展方法来安全地引发事件:

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

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

  

你们需要添加   [MethodImpl(MethodImplOptions.NoInlining)]   属于这些扩展方法   或者你试图复制   委托临时变量可以   被JITter优化掉,   允许空引用   异常。

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

我在Release模式下运行了一段时间,但从未遇到过NullReferenceException。

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

事实上,我猜真正的问题是,SaifeRaise会被列为:

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

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

3 个答案:

答案 0 :(得分:7)

问题不在于内联方法 - 无论是否内联,JITter都可以通过内存访问进行有趣的事情。

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

编辑:只是为了澄清,我明白为什么这会给你带来麻烦。你基本上有两个线程修改同一个变量(因为他们使用捕获的变量)。代码完全可能出现如下:

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!

在此代码中,这通常比通常稍微不那么明显,因为变量是捕获的变量而不是正常的静态/实例字段。同样的原则适用。

安全提升方法的目的是将引用存储在一个局部变量中,不能从任何其他线程修改

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

现在,线程2是否更改myEvent的值无关紧要 - 它无法更改处理程序的值,因此您将无法获得NullReferenceException

如果JIT 内联SafeRaise,它将被内联到此代码段 - 因为内联参数有效地结束为新的局部变量。只有当JIT 错误地通过保留两个单独的myEvent读取时,问题才会出现。

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

答案 1 :(得分:5)

这是内存模型问题。

基本上问题是:如果我的代码只包含一个逻辑读取,优化器可能会引入另一个读取吗?

令人惊讶的是,答案是:也许

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

因此,无论您使用局部变量还是参数,代码都不是线程安全的

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

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

答案 2 :(得分:1)

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