【问题标题】:thread safe construction with attribute assignment具有属性分配的线程安全构造
【发布时间】:2018-09-14 16:44:03
【问题描述】:

来自https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/object-and-collection-initializers

class Cat
{
    // Auto-implemented properties.
    public int Age { get; set; }
    public string Name { get; set; }
}

Cat cat = new Cat { Age = 10, Name = "Fluffy" };

对象初始值设定项语法允许您创建一个实例,并且 之后它将新创建的对象及其分配的属性分配给分配中的变量。

https://stackoverflow.com/a/19138412/432976

var albumData = new Album 
{
     Name = "Albumius",
     Artist = "Artistus",
     Year = 2013
 };

是这个等效代码的语法简写:

var albumData = new Album();
albumData.Name = "Albumius";
albumData.Artist = "Artistus";
albumData.Year = 2013;

编译后两者完全相同。

问题: 这种类型的构造+分配线程安全吗? (即,另一个读取 cat 的线程是否可以在 Cat 被创建和分配 Age 和 Name 之间看到它)?

第一个似乎是这样,因为它是在分配属性之后,变量被分配(线程安全顺序),第二个说在编译代码级别,顺序是不同的。

如果第二个是真的,下面的代码是否足以避免另一个线程看到一半构造的猫的竞争条件?

var myNewCat = new Cat { Age = 10, Name = "Fluffy" };
sharedCat = myNewCat;

我意识到这里存在次要竞争条件,即其他线程是否看到 oldCat 或 newCat,但在这种情况下,我唯一担心的是其他线程必须看到完整的 Cat,而不是半构造的 Cat。

【问题讨论】:

标签: c# multithreading constructor


【解决方案1】:

使用以下示例类

public class Cat
{
    public int Age { get; set; }
    public string Name { get; set; }
}

C# 2.0 风格

代码

var cat1 = new Cat();
cat1.Age = 10;
cat1.Name = "Fluffy";

生成以下IL 代码(使用.Net Reflector 检查)

L_000c: newobj instance void ConsoleApp1.Cat::.ctor()
L_0011: stloc.0 
L_0012: ldloc.0 
L_0013: ldc.i4.s 10
L_0015: callvirt instance void ConsoleApp1.Cat::set_Age(int32)
L_001a: nop 
L_001b: ldloc.0 
L_001c: ldstr "Fluffy"
L_0021: callvirt instance void ConsoleApp1.Cat::set_Name(string)
L_0026: nop 

这基本上创建了变量实例及其可用(来自stloc.0),因此如果它被暴露,另一个线程可以在该状态下获取它。

答案是否定的,这个版本不是线程安全的

C# 3.0 风格

从 C# 3 开始,我们可以做所谓的Object Initializers

代码

var cat1 = new Cat { Age = 10, Name = "Fluffy" };

生成以下 IL 代码

L_000c: newobj instance void ConsoleApp1.Cat::.ctor()
L_0011: dup 
L_0012: ldc.i4.s 10
L_0014: callvirt instance void ConsoleApp1.Cat::set_Age(int32)
L_0019: nop 
L_001a: dup 
L_001b: ldstr "Fluffy"
L_0020: callvirt instance void ConsoleApp1.Cat::set_Name(string)
L_0025: nop 
L_0026: stloc.0 

这里的主要区别是类的实例永远不会从评估堆栈中拉出,因为它使用dup,直到它全部完成然后stloc.0最后。

答案是这个方法是线程安全的

【讨论】:

    【解决方案2】:

    如果一段代码只以保证多个线程同时安全执行的方式操作共享数据结构,那么它就是线程安全的。 在您提供的示例中,没有共享状态,因为实例被分配给局部变量。因此,无论哪种方式(是的,它们是相同的)总是线程安全的:

    var albumData = new Album 
    {
         Name = "Albumius",
         Artist = "Artistus",
         Year = 2013
     };
    

    只要分配的变量不是其所在方法的本地变量(例如类的字段),情况就会发生变化:

    class TestClass
    {
      private Album album;
    
      public void TestAssignment(string name)
      {
        // Here I'm using this style of property assignment to make it very explicit why it's not thread-safe
        this.album = new Album();
        this.album.Name = name;
        ...
      }
    }
    

    如您所见,TestAssignment() 方法现在可以从不同的线程同时调用。根据执行每一行的线程,将为专辑实例及其名称分配不同的值。 在这种情况下,考虑线程安全机制是有意义的。

    希望这有助于澄清差异。

    【讨论】:

      【解决方案3】:

      @JonSkeet says 等效代码引入了一个临时变量。

      var tmp = new Album();
      tmp.Name = "Albumius";
      tmp.Artist = "Artistus";
      tmp.Year = 2013;
      var albumData = tmp;
      

      为了猫

      var tmp = new Cat();
      tmp.Age = 10;
      tmp.Name = "Fluffy";
      Cat cat = tmp;
      

      因此,如果引用分配是线程安全的,那么对象初始化器将是线程安全的。对吧?

      【讨论】:

        猜你喜欢
        • 2010-09-05
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多