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

Jef*_*Cyr 15 .net inline-method race-condition

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

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

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

但是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();
Run Code Online (Sandbox Code Playgroud)

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

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

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

while (true)
{
    EventHandler handler = myEvent;
    if (handler != null)
        handler(null, EventArgs.Empty);
}
Run Code Online (Sandbox Code Playgroud)

要么

while (true)
{
    if (myEvent != null)
        myEvent(null, EventArgs.Empty);
}
Run Code Online (Sandbox Code Playgroud)

Jon*_*eet 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!
Run Code Online (Sandbox Code Playgroud)

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

安全提升方法的要点是将引用存储在本地变量中,该变量不能从任何其他线程修改:

EventHandler handler = myEvent;
if (handler != null)
{
    handler(null, EventArgs.Empty);
}
Run Code Online (Sandbox Code Playgroud)

现在,线程2是否改变了值并不重要myEvent- 它不能改变处理程序的值,所以你不会得到一个NullReferenceException.

如果JIT 在线SafeRaise,它会被内联这个片段-因为内嵌参数最终为一个新的局部变量,有效.问题只有在JIT 通过保持两个单独的读取错误地内联它时myEvent.

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

  • @Daniel:我会看看,但我建议任何这样的优化都是完全疯狂的.如果你不能依赖于两个读取之间的*纯粹局部变量*的值,那么你可以依赖的不是很多. (2认同)

Dan*_*iel 5

这是一个内存模型问题.

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

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

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

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

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

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