如此问题所示: 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);
}
答案 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)
使用正确的代码,优化不应改变其语义。因此,如果错误不在代码中,优化器不会引入错误。