26

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

丹尼尔在他的回答中说(除非我读错了)ECMA-335 CLI规范可以允许编译器生成NullReferenceException从以下DoCallback方法抛出 a 的代码。

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();
    }
}

他说,为了保证 aNullReferenceException不被抛出,volatile关键字应该用 on_Callback或者 alock应该用在 line 周围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(() => { })();

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

易失性读取具有“获取语义”,这意味着读取保证发生在 CIL 指令序列中读取指令之后发生的任何对内存的引用之前。易失性写入具有“释放语义”,这意味着写入保证发生在 CIL 指令序列中写入指令之前的任何内存引用之后。CLI 的一致实现应保证 volatile 操作的这种语义。这确保了所有线程都将按照执行顺序观察任何其他线程执行的易失性写入。但是,从所有执行线程来看,一致的实现不需要提供易失性写入的单一总排序。

4

2 回答 2

13

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

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

Richter 建议使用Interlocked.CompareExchange<T>来明确解决此问题:

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

然而,Richter 承认微软的即时 (JIT) 编译器并没有优化掉局部变量。而且,尽管这在理论上可能会改变,但几乎可以肯定它永远不会改变,因为它会导致太多的应用程序因此而崩溃。

这个问题已经在“<a href="https://stackoverflow.com/questions/7664046/allowed-c-sharp-compiler-optimization-on-local-variables-and-refetching-value -fr">允许 C# 编译器对局部变量进行优化并从内存中重新获取值”。请务必阅读xanatox的答案和“<a href="http://msdn.microsoft.com/en-us/magazine/cc163715.aspx#S6" rel="nofollow noreferrer">了解低的影响- 它引用的多线程应用程序中的锁定技术”文章。由于您专门询问了 Mono,您应该注意引用的“<a href="http://www.mail-archive.com/mono-devel-list@lists.ximian.com/msg17446.html" rel=" nofollow noreferrer">[Mono-dev] 内存模型?” 邮件列表消息:

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

于 2012-05-15T21:16:50.210 回答
3

此代码不会引发空引用异常。这是线程安全的:

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

这个是线程安全的,并且不能在回调上抛出 NullReferenceException 的原因是它在进行空检查/调用之前复制到局部变量。即使在 null 检查后将原始 Callback 设置为 null,局部变量仍然有效。

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

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

在这一个中,它正在查看一个公共变量,Callback 可以更改为 null 之后,if (null != Callback)这会引发异常Callback();

于 2012-05-14T19:25:15.013 回答