我问这个问题,并得到了这个有趣的(有点令人不安)的答案。
丹尼尔在他的回答状态(除非我错误地读它)的ECMA-335 CLI规范可以让编译器生成抛出一个代码NullReferenceException
从以下DoCallback
方法。
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
不抛出, volatile
关键字应使用_Callback
或lock
应围绕行中使用local = Callback;
。
任何人都可以证实吗? 而且,如果这是真的,有没有就这个问题单和.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中的一致性实现应保证挥发性操作这个语义。 这确保了所有的线程将看到由他们执行的顺序任何其他线程执行挥发性写入。 但是,一个符合标准的实现并不需要提供从执行的所有线程看到挥发性写的一个整体排序。 该转换CIL到本机代码的优化编译器不得删除任何挥发性操作,也不得合并多种挥发性操作成一个单一的操作。