为什么(或不是)在构造函数中设置字段是线程安全的?

Why is (or isn't) setting fields in a constructor thread-safe?

假设您有一个像这样的简单 class:

class MyClass
{
    private readonly int a;
    private int b;

    public MyClass(int a, int b) { this.a = a; this.b = b; }

    public int A { get { return a; } }
    public int B { get { return b; } }
}

我可以以多线程方式使用这个 class:

MyClass value = null;
Task.Run(() => {
    while (true) { value = new MyClass(1, 1); Thread.Sleep(10); }
});
while (true)
{
    MyClass result = value;
    if (result != null && (result.A != 1 || result.B != 1)) { 
        throw new Exception(); 
    }
    Thread.Sleep(10);
}

我的问题是:我会看到这个(或其他类似的多线程代码)抛出异常吗?我经常看到其他线程可能不会立即看到非易失性写入这一事实。因此,这似乎可能会失败,因为对值字段的写入可能发生在对 a 和 b 的写入之前。这是可能的,还是内存模型中有什么东西可以使这个(非常常见的)模式安全?如果是这样,它是什么? readonly 为此目的是否重要?如果 a 和 b 是不能以原子方式编写的类型(例如自定义结构),这有关系吗?

This was wrong, sorry...

I think you could throw an error if your test if executed before the first assignment of the variable in the other thread. This would be a race condition... it may be an intermittent problem.

You could also get an error if the while loop checks the values at the exact moment that a new class is instantiated and assigned to value, but before the a and b variables are set.

至于更好的方法,这取决于您的目标。价值不断被覆盖是否有原因?我认为更常见的是将新的 类 放入一个需要按顺序处理的集合中。

有一些集合可以处理这个问题。您可以在一个线程中将 类 添加到集合中,并在另一个线程中检查并提取它们。

请参阅 http://dotnetcodr.com/2014/01/14/thread-safe-collections-in-net-concurrentstack/ 示例。

上述代码是线程安全的。构造函数在分配给 "value" 变量之前完全执行。第二个循环中的本地副本将是 null 或完全构造的实例,因为分配实例引用是内存中的原子操作。

如果 "value" 是一个结构,那么它就不是线程安全的,因为值的初始化不是原子的。

Thus, it seems like this could fail because the write to the value field might happen before the writes to a and b. Is this possible

是的,这当然是可能的。

您需要以某种方式同步对数据的访问以防止此类重新排序。

编写的代码将从 CLR2.0 开始运行,因为 CLR2.0 内存模型保证所有存储都具有释放语义

释放语义:确保在栅栏之前没有加载或存储 将在栅栏后移动。之后的说明可能还是发生在之前 围栏。(摘自 CPOW 第 512 页)。

这意味着构造函数初始化不能在class引用赋值之后移动。

Joe duffy 在他的 article about the very same subject 中提到了这一点。

Rule 2: All stores have release semantics, i.e. no load or store may move after one.

Vance morrison 的 article here 也证实了这一点(技术 4 节:惰性初始化)。

Like all techniques that remove read locks, the code in Figure 7 relies on strong write ordering. For example, this code would be incorrect in the ECMA memory model unless myValue was made volatile because the writes that initialize the LazyInitClass instance might be delayed until after the write to myValue, allowing the client of GetValue to read the uninitialized state. In the .NET Framework 2.0 model, the code works without volatile declarations.

从 CLR 2.0 开始,写入保证按顺序发生。它没有在 ECMA 标准中指定,它只是 CLR 的微软实现提供了这种保证。如果您 运行 在 CLR 1.0 或任何其他 CLR 实现中使用此代码,您的代码可能会出错

此更改背后的故事是:(来自 CPOW 第 516 页)

When the CLR 2.0 was ported to IA64, its initial development had happened on X86 processors, and so it was poorly equipped to deal with arbitrary store reordering (as permitted by IA64) . The same was true of most code written to target .NET by nonMicrosoft developers targeting Windows

The result was that a lot of code in the framework broke when run on IA64, particularly code having to do with the infamous double-checked locking pattern that suddenly didn't work properly. We'll examine this in the context of the pattern later in this chapter. But in summary, if stores can pass other stores, consider this: a thread might initialize a private object's fields and then publish a reference to it in a shared location; because stores can move around, another thread might be able to see the reference to the object, read it, and yet see the fields while they are still i n an uninitialized state. Not only did this impact existing code, it could violate type system properties such as initonly fields.

So the CLR architects made a decision to strengthen 2.0 by emitting all stores on IA64 as release fences. This gave all CLR programs stronger memory model behavior. This ensures that programmers needn' t have to worry about subtle race conditions that would only manifest in practice on an obscure, rarely used and expensive architecture.

注意 Joe duffy 说他们通过将 IA64 上的所有存储作为发布围栏发出来加强 2.0这并不意味着其他处理器可以对其重新排序。其他处理器本身固有地提供了存储存储(存储后跟存储)不会被重新排序的保证。所以CLR不需要明确保证这一点。

will I ever see this (or other similar multi-threaded code) throw an exception?


,在 ARM(以及任何其他内存模型较弱的硬件)上,您会观察到这种行为。

I often see reference to the fact that non-volatile writes might not immediately be seen by other threads. Thus, it seems like this could fail because the write to the value field might happen before the writes to a and b. Is this possible, or is there something in the memory model that makes this (quite common) pattern safe?

Volatile 与变化观察的瞬时性无关,它与顺序和 acquire/release 语义有关。
此外,ECMA-335 说它可能发生(并且它会发生在 ARM 或任何其他内存模型较弱的硬件上)。

Does readonly matter for this purpose?

readonly与指令重排序和易失性无关。

Would it matter if a and b were a type that can't be atomically written (e. g. a custom struct)?

在这种情况下,字段的原子性无关紧要。 为防止出现这种情况,您应该通过 Volatile.Write 编写对已创建对象的引用(或者只引用 volatile ,编译器将完成这项工作)。 Volatile.Write(ref value, new MyClass(1, 1)) 会成功的。

有关易失性语义和内存模型的更多信息,请参阅 ECMA-335,第 I.12.6 节

正如所写,这段代码是线程安全的,因为 value 在构造函数完成执行之前不会更新。换句话说,正在构建的对象不会被其他任何人观察到。

您可以编写代码,通过像

这样明确地向外界发布 this 来帮助您打自己的脸

class C { public C( ICObserver observer ) { observer.Observe(this); } }

当 Observe() 执行时,所有的赌注都被取消了,因为对象不被外界观察到的说法不再成立。