是否有用于预防的NullReferenceException这个共同模式的竞争状态?(Is ther

2019-06-24 10:34发布

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

丹尼尔在他的回答状态(除非我错误地读它)的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关键字应使用_Callbacklock应围绕行中使用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到本机代码的优化编译器不得删除任何挥发性操作,也不得合并多种挥发性操作成一个单一的操作。

Answer 1:

通过C#(。第264-265页)CLR,杰弗里里希特讨论这一特定问题,并承认它可能的被换出的局部变量:

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

里希特建议使用的Interlocked.CompareExchange<T>来明确地解决这个问题:

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

然而,里氏承认,微软只是在实时(JIT)编译器优化掉的本地变量; 并且,虽然这可能在理论上的变化,它几乎可以肯定永远不会,因为它会造成太多的应用,打破结果。

这个问题已经被问及“在长回答的局部变量允许C#编译器优化和重新获取来自内存值 ”。 请务必阅读答案xanatox和“ 理解低锁技术在多线程应用程序的影响 ”一文引用它。 既然你特别问了一下单,你要注意引用“ [ 单声道-dev的]内存模型? ”邮件列表的消息:

现在,我们提供接近由您正在运行的架构支持ECMA宽松的语义。



Answer 2:

此代码不会抛出一个空引用异常。 这一个是线程安全的:

public void DoCallback() {
    Action local;
    local = Callback;
    if (local == null)
        local = new Action(() => { });
    local();
}

之所以这一次是线程安全的,并能而不是扔在回调一个NullReferenceException,是它复制到一个局部变量这样做是空检查/调用之前。 即使原来的回调是空校验后设置为null,局部变量将仍然有效。

然而,下面是一个不同的故事:

public void DoCallbackIfElse() {
    if (null != Callback) Callback();
    else new Action(() => { })();
}

在这其中它在寻找一个公共变量,回调可以更改为空后的if (null != Callback)这将扔在一个异常Callback();



文章来源: Is there a race condition in this common pattern used to prevent NullReferenceException?