当前位置: 首页 > 知识库问答 >
问题:

此通用模式中是否存在用于防止空引用异常的竞争条件?

冀翰翮
2023-03-14

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

Daniel在他的回答中指出(除非我读错了),ECMA-335 CLI规范允许编译器从以下方法生成引发NullReferenceException的代码。

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

他说,为了保证不抛出NullReector ceException的易失性关键字应该在_Callback上使用,或者lock应该在local=Callback;行周围使用。

有人能证实这一点吗?如果这是真的,Mono和。NET编译器有关此问题的信息?

编辑此处是标准的链接。

更新我认为这是规范(12.6.4)的相关部分:

CLI的一致性实现可以使用任何技术自由执行程序,这些技术可以保证在单个执行线程内,线程生成的副作用和异常按照CIL指定的顺序可见。为此,只有易失性操作(包括易失性读取)才构成可见的副作用。(请注意,虽然只有易失性操作构成可见的副作用,但易失性操作也会影响非易失性引用的可见性。)挥发性操作在§12.6.7中有规定。对于另一个线程注入到线程中的异常,没有排序保证(此类异常有时称为“异步异常”(例如,System.Threading.ThreadAbortException)。

[理由:优化编译器可以自由地对副作用和同步异常进行重新排序,只要这种重新排序不会改变任何可观察到的程序行为。结束理由]

[注意:允许CLI的实现使用优化编译器,例如,将CIL转换为本机机器代码,前提是编译器保持(在每个执行线程中)相同的副作用和同步异常顺序。

所以我很好奇,这条语句是否允许编译器优化回调属性(访问一个简单字段)和局部变量以生成以下内容,这些内容在单个执行线程中具有相同的行为:

if (_Callback != null) _Callback();
else new Action(() => { })();

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

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

共有2个答案

万知
2023-03-14

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

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

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

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

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

在本例中,它查看的是一个公共变量,回调可以在if(null!=回调)之后更改为null,这将在回调()上引发异常

狄高畅
2023-03-14

在CLR中通过C#(第。

[T] 编译器可以对他的代码进行优化,以完全删除局部[…]变量。如果发生这种情况,此版本的代码与[直接引用事件/回调两次的版本]相同,因此仍然可能出现NullReferenceException。

里克特建议使用联锁。比较交换

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

然而,里希特承认微软的实时(JIT)编译器并没有优化掉局部变量;而且,虽然从理论上讲,这可能会改变,但几乎肯定永远不会改变,因为这将导致太多应用程序因此而中断。

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

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

 类似资料:
  • 我正在使用spring framework StringRedisTemplate来更新一个与多个线程发生的条目。 因此,handleSubmissions在每个分配ID中使用多个线程进行调用。但是每个主线程创建并调用两个反应性java线程,并处理与每个赋值相关联的提交列表。 在保持RxJava实现性能的同时,防止redis入口争用的最佳方法是什么?有没有一个方法我可以做这个redis操作更有效的

  • 在使用ACK时,有没有一种简单的方法实现类似于“锁定”的东西来防止RabbitMQ队列中的竞争条件? 我有以下问题--我有几个客户机使用ACK使用队列。每当客户端收到消息时,他就会确认并处理消息。但是,如果由于某种原因处理失败,我希望消息返回到队列。

  • 9.1. 竞争条件 在一个线性(就是说只有一个goroutine的)的程序中,程序的执行顺序只由程序的逻辑来决定。例如,我们有一段语句序列,第一个在第二个之前(废话),以此类推。在有两个或更多goroutine的程序中,每一个goroutine内的语句也是按照既定的顺序去执行的,但是一般情况下我们没法去知道分别位于两个goroutine的事件x和y的执行顺序,x是在y之前还是之后还是同时发生是没法

  • 我正在做分类帐模块。在这个过程中,我必须按顺序完成这些任务 从源中选择余额(table 1 row1) 从目标中选择余额(table 1 row2) 用一些逻辑修改余额 更新源的余额(table 1 row1) 更新目标余额(table 1 row2) 提交更改 将事务插入到事件表中。 在多线程环境中,线程在前一个线程更新和提交之前获得余额。在Postgres中,锁被强加给正在被访问的行,直到线程

  • 我们正在对网络套接字使用阻塞系统I/O调用。我们想要的行为是当对套接字调用时,阻塞调用需要返回并抛出一个异常(引用), 看看OpenJDK,这就是它的实现方式。它使用用户信号唤醒阻塞线程。相应的信号处理程序是no-op。在阻塞调用之前,它注册可能被阻塞的线程。当关闭文件描述符时,关闭线程向阻塞的I/O线程发送信号,这导致阻塞调用返回。 不过,我还是认为下面的代码块源代码中存在一个潜在的竞争条件:

  • 本节展示如何显示特定时间内用户空间锁竞争的情况。通过展示锁竞争的图景,你可以判断当前的性能问题是否由对futex的竞争所造成的。 简单地说,如果在同一时间内多个进程试图获取同一把锁,就会产生对futex的竞争。由于仅有一个进程可以持有锁,其他的进程都只能等待锁重新可用,锁竞争会导致性能的下降。 下面的futexes.stp脚本通过探测futex系统调用来显示锁竞争的情况: futexes.stp