2

参考

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

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

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

myDelegate?.Invoke()

相当于:

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

我的问题是,这种行为与说 a是否相同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
}
4

4 回答 4

8

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

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

这是一个非常危险的位置,我强烈劝阻你不要追求这个架构决策。寻找另一种在工作人员之间共享内存的方法。

要解决您的问题:

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

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

您没有问的第二个问题是: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 变量进行读写的顺序。也就是说,如果内存模型允许两种可能的观察顺序,那么一个线程观察一个,另一个线程观察另一个是合法的。如果您的程序逻辑隐含地依赖于观察到的单一读写顺序,那么您的程序是错误的.

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

那你该怎么办?

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

答案是肯定的。

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

编译为以下内容(如此处所示

List<object> list = new List<object>();
if (list != null)
{
    list.Add(new object());
}
于 2019-07-10T03:37:47.857 回答
2

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

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());
        }
    }
}
于 2019-07-10T04:15:56.147 回答
0

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

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
于 2019-07-10T03:51:17.463 回答