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

时间:2012-05-14 19:08:20

标签: c# .net multithreading thread-safety il

我问了 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到本机代码不应删除任何易失操作,也不应删除   它将多个易失性操作合并为一个操作。

2 个答案:

答案 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();上引发异常