【问题标题】:Does the null-conditional operator function the same with delegates and regular objects?空条件运算符与委托和常规对象的功能相同吗?
【发布时间】:2019-11-19 15:08:00
【问题描述】:

Reference

我目前正在处理一些线程敏感代码。

在我的代码中,我有一个由两个不同线程操作的对象列表。一个线程可以将对象添加到此列表中,而另一个线程可以将其设置为 null。

在上面的参考资料中,它特别提到了代表:

myDelegate?.Invoke()

相当于:

var handler = myDelegate;
if (handler != null)
{
    handler(…);
}

我的问题是,List<> 的这种行为是否相同?例如:

是:

var myList = new List<object>();    
myList?.Add(new object());

保证等价于:

var myList = new List<object>();

var tempList = myList;
if (tempList != null)
{
    tempList.Add(new object());
}

?


编辑:

请注意,(委托的工作方式)之间存在差异:

var myList = new List<int>();
var tempList = myList;
if (tempList != null)
{
    myList = null; // another thread sets myList to null here
    tempList.Add(1); // doesn't crash
}

var myList = new List<int>();
if (myList != null)
{
    myList = null; // another thread sets myList to null here
    myList.Add(1); // crashes
}

【问题讨论】:

  • 为什么不直接编写代码并测试您的假设是否正确?
  • 我不知道如何检验我的假设,在 TheGeneral 的回答之前我不知道如何查看编译后的代码。
  • 至于线程安全部分,您​​可能需要阅读this,因为您可能想了解与委托有关的竞争条件。
  • @Enigmativity:问题是“在这种情况下,编译器、抖动或 CPU 是否有可能引入额外的读取?”您不能编写任何代码来测试该命题!您只能编写代码来测试“这个特定的编译器、抖动和 CPU 在我运行代码的那天是否没有引入额外的读取?”这不是需要回答的问题。
  • @EricLippert - 谢谢你,埃里克。在座的所有人中,你是我喜欢,甚至期待被纠正的一个人。 :-)

标签: c# .net delegates null-conditional-operator


【解决方案1】:

这是一个需要仔细分析的微妙问题。

首先,问题中提出的代码毫无意义,因为它对保证不为空的局部变量进行空检查。据推测,真正的代码从一个非局部变量中读取,该变量可能为空也可能不为空,并且可能在多个线程上被更改。

这是一个超级危险的职位,我强烈建议您不要追求这个架构决策。寻找另一种在工作人员之间共享内存的方法。

解决您的问题:

问题的第一个版本是:?. 运算符是否与您引入临时的版本具有相同的语义?

是的,确实如此。但我们还没有完成。

您没有问的第二个问题是:C# 编译器、抖动或 CPU 是否有可能导致具有临时性的版本引入额外的读取?也就是说,我们是否保证

var tempList = someListThatCouldBeNull;
if (tempList != null)
    tempList.Add(new object());

永远不会像你写的那样执行

var tempList = someListThatCouldBeNull;
if (tempList != null) 
    someListThatCouldBeNull.Add(new object());

“引入读取”的问题在 C# 中很复杂,但简短的版本是:一般来说您可以假设不会以这种方式引入读取。

我们好吗?当然不是。该代码完全不是线程安全的,因为Add 可能会在多个线程上被调用,这是未定义的行为!

假设我们以某种方式解决了这个问题。现在情况好吗?

没有。我们仍然不应该对这段代码有信心。

为什么不呢?

原始发布者没有显示任何机制来保证someListThatCouldBeNull 的最新值正在被读取。 它是在锁下访问的吗?它易挥发吗?是否引入了内存屏障? C# 规范非常清楚,如果不涉及锁或易失性等特殊效果,读取可以在时间上任意向后移动。您可能正在读取缓存值。

同样,我们也没有看到执行写入的代码;这些写入可以任意移动到未来。移至过去的读取或移至未来的写入的任何组合都可能导致读取“陈旧”值。

现在假设我们解决了这个问题。这能解决整个问题吗?当然不是。 我们不知道涉及多少线程,或者这些线程中是否有任何线程也在读取相关变量,以及这些读取是否存在任何假定的排序限制。 C# 确实要求所有读取和写入的顺序具有全局一致的视图!两个线程可能不同意对 volatile 变量的读取和写入顺序。也就是说,如果内存模型允许两种可能的观察顺序,则一个线程观察一个,另一个线程观察另一个是合法的。 如果您的程序逻辑隐含地依赖于观察到的单一读写顺序,那么您的程序是错误的

现在也许你明白为什么我强烈建议不要以这种方式共享内存。这是一个充满微妙错误的雷区。

那你该怎么办?

  • 如果可以的话:停止使用线程。找到一种不同的方式来处理您的异步。
  • 如果您做不到,使用线程作为解决问题的工作线程,然后返回池。让两个线程同时敲击同一个内存是很难做到的。让一个线程启动并计算一些东西并在完成后返回值更容易正确处理,而且您可以...
  • ...使用任务并行库或其他旨在正确管理线程间通信的工具。
  • 如果您做不到,尝试尽可能少地改变变量。不要将变量设置为空。如果您要填写列表,请使用线程安全列表类型初始化列表一次,然后仅从该变量中读取。让列表对象为您处理线程问题。

【讨论】:

    【解决方案2】:

    this answer 中,Eric Lippert 确认在所有情况下都使用临时变量,这将阻止“?”。运算符导致 NullReferenceException 或访问两个不同的对象。但是,还有许多其他因素会导致此代码不是线程安全的,请参阅Eric's answer

    UPD:解决未创建临时变量的声明:无需为局部变量引入临时变量。但是,如果您尝试访问可能会被修改的内容,则会创建一个变量。使用相同的 SharpLab 并稍微修改代码,我们得到:

    using System;
    using System.Collections.Generic;
    
    public class C {
        public List<Object> mList;
    
        public void M() {
            this.mList?.Add(new object());
        }
    }
    

    变成

    public class C
    {
        public List<object> mList;
    
        public void M()
        {
            List<object> list = mList;
            if (list != null)
            {
                list.Add(new object());
            }
        }
    }
    

    【讨论】:

    • 其他答案中的编译代码似乎并非如此,我不知道该相信谁!埃里克的回答是 3 岁,所以事情可能已经改变了:/
    • @Loocid 编译的示例并没有显示您认为它们显示的内容。它们不会引入临时变量,因为您正在访问一个局部变量,并且代码之外的任何人都无法修改它。
    • 宾果游戏,你是 100% 正确的。这确实有道理。一旦我将 myList 移出这些示例的范围,它就会创建临时列表。
    • 我没有任何确认。我确认使用了一个临时的,是的,但该答案的重点是生成的操作可能仍然不安全。操作的安全性不仅取决于没有引入额外的读取,而且取决于正确处理竞争条件,其中删除的事件处理程序的副作用与事件处理程序的执行竞争。跨度>
    • @EricLippert 很抱歉过于简化你的话。我并不是说使用 Elvis 操作符神奇地使代码线程安全。显然可能有很多陷阱。但是 OP 只关心对象(甚至不是处理程序)变为 null 的可能性,可以这么说,在“?”之间。和“.”,这就是我所说的“for your purpose”。让我在回答中澄清这一点。
    【解决方案3】:

    答案是肯定的。

    var myList = new List<object>();    
    myList?.Add(new object());
    

    编译为以下 (as seen here)

    List<object> list = new List<object>();
    if (list != null)
    {
        list.Add(new object());
    }
    

    【讨论】:

    • 其实,现在看,并不等同于delegate。请注意,委托使用临时变量,而 List 没有。
    【解决方案4】:

    是的,它们是一样的。你还可以看到下面的底层IL,由Ildasm生成:

    public void M()
    {
        var myList = new List<object>();
        myList?.Add(new object());
    }
    

    这将是:

    .method public hidebysig instance void  M() cil managed
    {
      // Code size       25 (0x19)
      .maxstack  2
      .locals init (class [System.Collections]System.Collections.Generic.List`1<object> V_0)
      IL_0000:  nop
      IL_0001:  newobj     instance void class [System.Collections]System.Collections.Generic.List`1<object>::.ctor()
      IL_0006:  stloc.0
      IL_0007:  ldloc.0
      IL_0008:  brtrue.s   IL_000c
      IL_000a:  br.s       IL_0018
      IL_000c:  ldloc.0
      IL_000d:  newobj     instance void [System.Runtime]System.Object::.ctor()
      IL_0012:  call       instance void class [System.Collections]System.Collections.Generic.List`1<object>::Add(!0)
      IL_0017:  nop
      IL_0018:  ret
    } // end of method C::M
    

    还有:

    public void M2()
    {
        List<object> list = new List<object>();
        if (list != null)
        {
            list.Add(new object());
        }
    }
    

    这将是:

    .method public hidebysig instance void  M2() cil managed
    {
      // Code size       30 (0x1e)
      .maxstack  2
      .locals init (class [System.Collections]System.Collections.Generic.List`1<object> V_0,
               bool V_1)
      IL_0000:  nop
      IL_0001:  newobj     instance void class [System.Collections]System.Collections.Generic.List`1<object>::.ctor()
      IL_0006:  stloc.0
      IL_0007:  ldloc.0
      IL_0008:  ldnull
      IL_0009:  cgt.un
      IL_000b:  stloc.1
      IL_000c:  ldloc.1
      IL_000d:  brfalse.s  IL_001d
      IL_000f:  nop
      IL_0010:  ldloc.0
      IL_0011:  newobj     instance void [System.Runtime]System.Object::.ctor()
      IL_0016:  callvirt   instance void class [System.Collections]System.Collections.Generic.List`1<object>::Add(!0)
      IL_001b:  nop
      IL_001c:  nop
      IL_001d:  ret
    } // end of method C::M2
    

    【讨论】:

    • 和我对@TheGeneral 说的一样。这不等同于委托。委托在被调用之前被分配给一个临时变量。列表不是。
    猜你喜欢
    • 2012-03-18
    • 1970-01-01
    • 1970-01-01
    • 2012-05-04
    • 2013-01-05
    • 1970-01-01
    • 1970-01-01
    • 2017-12-01
    • 2021-12-03
    相关资源
    最近更新 更多