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)
问题不在于内联方法 - 无论是否内联,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.
现在,至于为什么你只看到这种情况发生在调试模式:我怀疑附加调试器,线程相互中断的空间更大.可能还发生了一些其他优化 - 但它没有引入任何破损,所以没关系.
这是一个内存模型问题.
基本上问题是:如果我的代码只包含一个逻辑读取,优化器可能会引入另一个读取吗?
令人惊讶的是,答案是:也许
在CLR规范中,没有什么能阻止优化器执行此操作.优化不会破坏单线程语义,并且只保证为易失性字段保留内存访问模式(即使这是一个不是100%真实的简化).
因此,无论您使用局部变量还是参数,代码都不是线程安全的.
但是,Microsoft .NET框架记录了不同的内存模型.在该模型中,不允许优化器引入读取,并且您的代码是安全的(独立于内联优化).
也就是说,使用[MethodImplOptions]似乎是一个奇怪的黑客,因为阻止优化器引入读取只是不内联的副作用.我会使用volatile字段或Thread.VolatileRead.
| 归档时间: |
|
| 查看次数: |
674 次 |
| 最近记录: |