是否存在用于防止NullReferenceException的常见模式中的竞争条件?

我问了这个问题并得到了这个有趣(并且有点令人不安)的答案。

Daniel在他的回答中指出(除非我读错了) ECMA-335 CLI规范允许编译器生成从以下DoCallback方法抛出NullReferenceException代码。

 class MyClass { private Action _Callback; public Action Callback { get { return _Callback; } set { _Callback = value; } } public void DoCallback() { Action local; local = Callback; if (local == null) local = new Action(() => { }); local(); } } 

他说,为了保证不抛出NullReferenceException ,应该在_Callback上使用volatile关键字,或者在local = Callback;行周围使用lock local = Callback;

任何人都可以证实这一点吗? 而且,如果确实如此,那么Mono.NET编译器在这个问题上的行为是否存在差异?

编辑
这是标准的链接。

更新
我认为这是规范的相关部分(12.6.4):

符合CLI的实现可以使用任何技术自由执行程序,这些技术可以在单个执行线程中保证线程生成的副作用和exception按照CIL指定的顺序可见。 为此目的,仅挥发性操作(包括挥发性读取)构成可见的副作用。 (请注意,虽然只有易失性操作构成可见的副作用,但易失性操作也会影响非易失性引用的可见性。)易失性操作在第12.6.7节中规定。 相对于另一个线程注入线程的exception,没有排序保证(此类exception有时称为“异步exception”(例如,System.Threading.ThreadAbortException)。

[基本原理:优化编译器可以自由地重新排序副作用和同步exception,只要这种重新排序不会改变任何可观察的程序行为。 最终理由]

[注意:允许CLI的实现使用优化编译器,例如,将CIL转换为本机机器代码,前提是编译器维护(在每个执行线程内)相同的副作用和同步exception顺序。

所以…我很好奇这个语句是否允许编译器优化Callback属性(访问一个简单字段)和local变量来产生以下内容,它在单个执行线程中具有相同的行为:

 if (_Callback != null) _Callback(); else new Action(() => { })(); 

volatile关键字的12.6.7部分似乎为希望避免优化的程序员提供了一个解决方案:

易失性读取具有“获取语义”,这意味着保证在对CIL指令序列中的读取指令之后发生的对存储器的任何引用之前发生读取。 易失性写入具有“释放语义”,这意味着写入保证在CIL指令序列中的写入指令之前的任何存储器引用之后发生。 CLI的一致性实现应保证volatile操作的语义。 这可确保所有线程将按照执行顺序观察由任何其他线程执行的易失性写入。 但是,从所有执行线程看,不需要符合要求的实现来提供易失写入的单个总排序。 将CIL转换为本机代码的优化编译器不应删除任何易失性操作,也不应将多个易失性操作合并为单个操作。

CLR via C# (pp.264-265)中,Jeffrey Richter讨论了这个特定问题,并承认局部变量可能被换出:

[T]编译器可以优化他的代码以完全删除本地变量。 如果发生这种情况,此版本的代码与[直接引用事件/回调两次的版本]相同,因此仍然可以使用NullReferenceException

Richter建议使用Interlocked.CompareExchange来明确解决此问题:

 public void DoCallback() { Action local = Interlocked.CompareExchange(ref _Callback, null, null); if (local != null) local(); } 

但是,Richter承认微软的即时(JIT)编译器并没有优化掉局部变量; 虽然从理论上讲,这可能会发生变化,但几乎肯定不会发生变化,因为它会导致太多的应用程序因此而中断。

已经在“ 允许C#编译器优化局部变量并从内存中重新获取值 ”中详细询问并回答了这个问题。 请务必阅读xanatox的回答以及“ 了解multithreading应用程序中低锁技术的影响 ”文章。 既然你专门询问了Mono,你应该注意引用“ [Mono-dev]内存模型? “邮件列表消息:

现在,我们提供了与您正在运行的体系结构支持的ecma接近的松散语义。

此代码不会抛出空引用exception。 这个是线程安全的:

 public void DoCallback() { Action local; local = Callback; if (local == null) local = new Action(() => { }); local(); } 

这个是线程安全的,并且不能在Callback上抛出NullReferenceException,是因为它在执行null check / call之前复制到局部变量。 即使在null检查后将原始Callback设置为null,本地变量仍然有效。

但是以下是一个不同的故事:

 public void DoCallbackIfElse() { if (null != Callback) Callback(); else new Action(() => { })(); } 

在这一个中,它正在查看一个公共变量,在if (null != Callback)之后可以将Callback更改为null,这将在Callback();上抛出exceptionCallback();