.Net POCO是否是线程安全的?

5
这个问题可能看起来有点奇怪,但与可能的可见性问题有关。这个问题是受Java编程语言(>jdk5)中一个案例启发的,请考虑:
public class InmutableValue {
  private int value;
  public InmutableValue(int value) {this.value = value;}
  public int getValue() {return value;}
}

与常见观念相反,上面的类并不是线程安全的。在多线程环境中,“value”不能保证对其他线程可见。为了使其线程安全,我们需要强制执行一个“happens-before”规则,在字段中标记为“final”即可实现。

这种情况让我想知道 .Net 运行时是否也是如此。例如:

public class InmutableValue {
  private int value;
  public InmutableValue(int value) {this.value = value;}
  public int Value { get{return value;}}
} 

据我所知,将值字段标记为“readonly”并不能像Java中的“final”一样提供相同的保证(但我可能非常错误,希望如此)。因此,我们是否需要将字段标记为“volatile”(或使用内存屏障等)以确保对其他线程的可见性?或者是否应用其他规则以确保可见性?

1
value 不会被 InmutableValue 类之外的代码所看到,因为它是 private 的。不过,这似乎不是您使用的“visible”的定义。 - Gabe
不,我的意思是:在没有使用内存屏障或其他同步手段的情况下,该值是否对其他线程可见?与Java的一个区别是,在CLR中,构造函数仅在字段被分配后才返回其指针。因此,在.Net中不存在部分初始化的对象,这与Java不同。 - M Platvoet
我要展示我的无知,但是“POCO”即“普通的CLR对象”不意味着任何给定对象的线程安全性与EF无关吗?它们的定义是类本身没有特殊的EF逻辑,因此所有在CLR中编程时相同的线程安全问题仍然适用。 - Tom W
1
我很难弄清楚这里真正被问的是什么。 - Andrew Barber
你是指 Entity Framework 吗?那不涉及到这个问题。看起来当内部字段是易失性的或者在访问时应用了内存屏障时,不可变对象才能安全地在多个线程之间共享。但我无法确认,希望有人可以证实。 - M Platvoet
显示剩余2条评论
2个回答

2
您可能会担心cpu核心的内存模型,例如Alpha和Titanium。它们具有可以重新排序内存写入的内存写入缓冲区,从而可能在value字段值之前写入对象引用。也许是受到Raymond Chen博客的启发?
但您错过了一个重要细节。要创建线程竞争,必须有两个线程都使用对象引用。一个创建对象并存储引用,另一个使用该引用。这是根本上不安全的,共享对象引用的访问需要同步。同步代码(如lock语句)还确保刷新写回缓冲区。这可以防止读取属性的线程看到旧值。

基本上,即使POJO是不可变的,它也不是线程安全的,只有通过声明字段为volatile或其他同步代码手段才能使其线程安全。.Net在readonly方面提供的同步级别与Java中的final不同? - M Platvoet
你完全没有理解我想要解释的内容。在线程之间共享“对象引用”是不安全的,无论使用什么语言,缩写都一样。 - Hans Passant
那么在线程之间共享一个静态初始化对象是不安全的吗? - M Platvoet
没有区别,readonly 变量在构造函数中被初始化,在执行主体语句之前。 - Hans Passant
没错。对象初始化的线程安全性是无关紧要的,因为任何“正确”的程序在共享对象引用时都会使用某种形式的同步,从而确保未初始化的对象不会被其他线程看到。当您需要在“不正确”的程序中使用对象时,像Java final一样的线程安全初始化只有在您需要维护某些属性(比如安全沙箱)时才相关。 - Daniel
@Daniel:有时候一个线程存储一个引用,另一个线程读取它是合法的,而不需要同步,如果读者无论看到新值还是旧值都能正确工作。问题在于,在某些常见情况下,需要哪些内存屏障才能确保正确性。 - supercat

1

无论如何,readonly关键字只确保字段在构造函数中被分配,而不是在类的其他地方。

至于构造函数是否在一般意义上是线程安全的,一旦创建对象,它将返回相同的值:

static ImmutableValue imv = new ImmutableValue(123);

// thread 1, object is not created
imv.Value; // NullReferenceException, as usually

// thread 2, object is created
imv.Value; //123

// thread 3, object is created
imv.Value; //123

要编辑private字段,您需要使用反射。是的,在执行此类代码期间,您需要锁定对象以保证冻结所有尝试读取该值的线程。


这是否意味着要回答提供的源代码关于线程安全性的问题? - Darin Dimitrov
@Darin:你只看到了我回答的一部分。 - abatishchev
我看到了你发布的内容。现在你添加了第二部分,它变得更好了 :-) 那么你的结论是这段代码在 .NET 上是100%线程安全的? - Darin Dimitrov
我知道,Java的final关键字也是如此,尽管Java final可以解决可见性问题。顺便说一句,你对readonly的陈述并不完全正确。readonly字段也可以通过一个称为“beforefieldinit”的特殊标记进行初始化,该标记在没有构造函数的情况下初始化字段;-) - M Platvoet
1
“readonly”关键字仅确保字段仅在构造函数中分配,而不是其他任何位置 - 这只是真相的一半。 readonly 关键字还可以保护自定义结构的成员:通过编译器警告进行直接更改,并通过成员方法在运行时进行间接更改。 - springy76

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接