丹尼尔在他的回答中说(除非我读错了)ECMA-335 CLI规范可以允许编译器生成NullReferenceException
从以下DoCallback
方法抛出 a 的代码。
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();
}
}
他说,为了保证 aNullReferenceException
不被抛出,volatile
关键字应该用 on_Callback
或者 alock
应该用在 line 周围local = Callback;
。
任何人都可以证实这一点吗?而且,如果这是真的, Mono和.NET编译器在此问题上的行为是否存在差异?
编辑
这里是标准的链接。
更新
我认为这是规范(12.6.4)的相关部分:
符合 CLI 的实现可以使用任何技术自由执行程序,这些技术保证在单个执行线程内,线程生成的副作用和异常按照 CIL 指定的顺序可见。为此,只有易失性操作(包括易失性读取)构成可见的副作用。(请注意,虽然只有易失性操作构成可见的副作用,但易失性操作也会影响非易失性引用的可见性。)易失性操作在第 12.6.7 节中指定。对于由另一个线程注入到一个线程的异常没有顺序保证(这种异常有时称为“异步异常”(例如,System.Threading.ThreadAbortException)。
[基本原理:优化编译器可以自由地重新排序副作用和同步异常,只要这种重新排序不会改变任何可观察的程序行为。结束理由]
[注意:允许 CLI 的实现使用优化编译器,例如,将 CIL 转换为本机机器代码,前提是编译器(在每个单个执行线程内)保持相同顺序的副作用和同步异常。
所以......我很好奇这个语句是否允许编译器优化Callback
属性(它访问一个简单的字段)和local
变量以产生以下内容,它在单个执行线程中具有相同的行为:
if (_Callback != null) _Callback();
else new Action(() => { })();
关键字的 12.6.7 部分volatile
似乎为希望避免优化的程序员提供了一个解决方案:
易失性读取具有“获取语义”,这意味着读取保证发生在 CIL 指令序列中读取指令之后发生的任何对内存的引用之前。易失性写入具有“释放语义”,这意味着写入保证发生在 CIL 指令序列中写入指令之前的任何内存引用之后。CLI 的一致实现应保证 volatile 操作的这种语义。这确保了所有线程都将按照执行顺序观察任何其他线程执行的易失性写入。但是,从所有执行线程来看,一致的实现不需要提供易失性写入的单一总排序。