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

ken*_*ken 25 .net c# il multithreading thread-safety

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

Daniel在他的回答中指出(除非我读错了)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();
    }
}
Run Code Online (Sandbox Code Playgroud)

他说,为了保证NullReferenceException不抛出,volatile关键字应该用于_Callbacklock应该在线周围使用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(() => { })();
Run Code Online (Sandbox Code Playgroud)

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

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

Dou*_*las 12

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

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

里希特建议使用Interlocked.CompareExchange<T>最终解决这个问题:

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

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

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

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