我问了 this 问题,并且 this 有趣(并且有点令人不安)答案。
Daniel在他的回答中指出(除非我读错了) ECMA-335 CLI 规范允许编译器生成从以下{NullReferenceException
生成DoCallback
的代码{1}}方法。
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;
。
有人可以证实这一点吗?而且,如果确实如此, Mono 和 .NET 编译器在这个问题上的行为是否存在差异?
修改
以下是 standard 的链接。
更新
我认为这是规范的相关部分(12.6.4):
符合CLI的实现可以自由执行程序 在单个线程中使用任何保证的技术 执行,线程产生的副作用和异常 以CIL指定的顺序可见。仅为此目的 易失性操作(包括易失性读取)构成可见 副作用。 (注意,虽然只有易失性操作构成 可见的副作用,挥发性操作也会影响能见度 非易失性引用。)挥发性操作在 §12.6.7。相对于异常没有排序保证 由另一个线程注入一个线程(例如 有时称为“异步异常”(例如, System.Threading.ThreadAbortException)。
[理由:优化 编译器可以自由地重新排序副作用和同步异常 此重新排序的程度不会改变任何可观察的程序 行为。最终理由]
[注意:CLI的实现是 允许使用优化编译器,例如,转换CIL 编译器维护的本机机器代码(在每个机器人内部) 单个执行线程)相同的副作用和顺序 同步异常。
所以...我很好奇这个语句是否允许编译器优化Callback
属性(访问一个简单字段)和local
变量来生成以下内容,在单个执行线程中具有相同行为 :
if (_Callback != null) _Callback();
else new Action(() => { })();
volatile
关键字上的12.6.7部分似乎为希望避免优化的程序员提供了解决方案:
易失性读取具有“获取语义”,意味着读取是 保证在之后发生的任何内存引用之前发生 CIL指令序列中的读指令。一个易变的写 具有“释放语义”意味着写保证会发生 在CIL中写入指令之前的任何内存引用之后 指令序列。 CLI的一致性实现应 保证volatile操作的这种语义。这确保了所有 线程将观察由任何其他线程执行的易失性写入 他们执行的顺序。但是,符合要求的实施不是 如图所示,需要提供易失写入的单个总排序 来自所有执行线程。转换的优化编译器 CIL到本机代码不应删除任何易失操作,也不应删除 它将多个易失性操作合并为一个操作。
答案 0 :(得分:12)
在 CLR中通过C#(第264-265页),Jeffrey Richter讨论了这个具体问题,并承认 可以将局部变量换出:
[T]编译器可以优化他的代码以完全删除本地变量。如果发生这种情况,此版本的代码与[直接引用事件/回调两次的版本]相同,因此仍然可以使用
NullReferenceException
。
Richter建议使用Interlocked.CompareExchange<T>
来明确解决此问题:
public void DoCallback()
{
Action local = Interlocked.CompareExchange(ref _Callback, null, null);
if (local != null)
local();
}
然而,Richter承认微软的即时(JIT)编译器不优化掉局部变量;而且,虽然理论上这可能会发生变化,但几乎肯定不会发生变化,因为它会导致过多的应用程序因此而中断。
这个问题已在“Allowed C# Compiler optimization on local variables and refetching value from memory”中详细询问并回答。请务必阅读xanatox及其引用的“Understand the Impact of Low-Lock Techniques in Multithreaded Apps”文章。由于您专门询问了Mono,因此您应该注意引用的“[Mono-dev] Memory Model?”邮件列表消息:
现在,我们提供了与您正在运行的体系结构支持的ecma接近的松散语义。
答案 1 :(得分:3)
此代码将不抛出空引用异常。这个是线程安全的:
public void DoCallback() {
Action local;
local = Callback;
if (local == null)
local = new Action(() => { });
local();
}
这个是线程安全的,并且不能在Callback上抛出NullReferenceException,是因为它在执行null check / call之前复制到局部变量。即使在空检查之后将原始Callback设置为null,局部变量仍然有效。
然而以下是一个不同的故事:
public void DoCallbackIfElse() {
if (null != Callback) Callback();
else new Action(() => { })();
}
在这一个中,它正在查看一个公共变量,在if (null != Callback)
之后可以将Callback更改为null,这会在Callback();
上引发异常