为什么(或不是)在构造函数中设置字段是线程安全的? [英] Why is (or isn't) setting fields in a constructor thread-safe?

查看:493
本文介绍了为什么(或不是)在构造函数中设置字段是线程安全的?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

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; } }
}

我可以以多线程方式使用此类:

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);
}

我的问题是:我会看到这个(或其他类似的多线程代码)抛出异常吗?我经常看到这样的事实,即非易失性写入可能不会立即被其他线程看到.因此,似乎这可能会失败,因为对value字段的写入可能在对a和b的写入之前发生.这是否可能,或者内存模型中是否有某种东西可以使这种(相当普遍的)模式安全?如果是这样,那是什么?为此,只读是否重要?如果a和b是无法原子写的类型(例如,自定义结构),会不会很重要?

解决方案

编写的代码将从CLR2.0开始工作,因为CLR2.0内存模型可以保证所有存储库都具有发布语义. /p>

发布语义:确保没有任何安全栅之前的加载或存储 将在篱笆后面移动.之后的指示可能仍会在之前发生 (摘自 CPOW 第512页).

这意味着在分配了类引用之后,无法移动构造函数初始化.

乔·达菲(Joe duffy)在有关同一主题的文章中提到了这一点.

规则2:所有商店都具有发布语义,即不得加载或存储 一个接一个地移动.

另外,万斯·莫里森的此处的文章确认了相同的内容(第4节) :延迟初始化).

与所有删除读取锁的技术一样,图7中的代码 依靠强大的写入顺序.例如,此代码将是 除非将myValue设置为volatile,否则ECMA内存模型中的错误 因为初始化LazyInitClass实例的写操作可能是 延迟到写入myValue之后,才允许 GetValue读取未初始化状态.在.NET Framework 2.0中 模型,代码无需可变声明即可工作.

保证从CLR 2.0开始按顺序进行写操作.它在ECMA标准中未指定,只是CLR的Microsoft实现提供了此保证.如果您在CLR 1.0或任何其他CLR实现中运行此代码,您的代码很可能会破坏.

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

当CLR 2.0移植到IA64时,其最初的开发已经 发生在X86处理器上,因此它的处理能力很差 任意商店重新排序(由IA64允许).也是一样 非Microsoft开发人员将大多数代码写入目标.NET 定位Windows

结果是,当在Windows 2000上运行时,框架中的许多代码都中断了 IA64,尤其是与臭名昭著的双重检查有关的代码 突然无法正常工作的锁定模式.我们将对此进行检查 在本章后面的模式中.但总而言之, 如果商店可以通过其他商店,请考虑以下事项:线程可能 初始化私有对象的字段,然后发布对 它在一个共享的位置;因为商店可以四处走动 线程可能能够看到对该对象的引用,并对其进行读取,并且 尚未看到字段时,它们仍处于未初始化状态. 这不仅会影响现有代码,而且可能会违反类型系统 属性,例如initonly字段.

因此,CLR架构师决定通过发布2.0来增强2.0 IA64上的所有商店都用作发布围栏.这给了所有CLR程序 更强的内存模型行为.这确保了程序员不需要 不得不担心只会在以下情况下表现出来的微妙的比赛条件 在一种晦涩,很少使用且昂贵的架构上进行实践.

请注意,乔·达菲(Joe duffy)表示,他们通过发布IA64上的所有存储作为发布屏障来加强2.0版 ,但这并不意味着其他处理器可以对其进行重新排序.其他处理器本身固有地提供了保证store-store(store-store后跟store)不会被重新排序的保证.因此,CLR无需明确保证这一点.

Let's say you have a simple class like this:

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; } }
}

I could use this class in a multi-threaded manner:

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);
}

My question is: will I ever see this (or other similar multi-threaded code) throw an exception? 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? If so, what is it? Does readonly matter for this purpose? Would it matter if a and b were a type that can't be atomically written (e. g. a custom struct)?

解决方案

Code as written will work starting from CLR2.0 as the CLR2.0 memory model guarantees that All stores have release semantics.

Release semantics: Ensures no load or store that comes before the fence will move after the fence. Instructions after it may still happen before the fence.(Taken from CPOW Page 512).

Which means that constructor initialization cannot be moved after the assignment of the class reference.

Joe duffy mentioned this in his article about the very same subject.

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

Also Vance morrison's article here confirms the same(Section Technique 4: Lazy Initialization).

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.

Writes are guaranteed to happen in order starting from CLR 2.0. It is not specified in ECMA standard, it is just the microsoft implementation of the CLR gives this guarantee. If you run this code in either CLR 1.0 or any other implementation of CLR, your code is likely to break.

Story behind this change is:(From CPOW Page 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.

Note Joe duffy says that they strengthen 2.0 by emitting all stores on IA64 as release fences which doesn't mean that other processors can reorder it. Other processors itself inherently provides the guarantee that store-store(store followed by store) will not be reordered. So CLR doesn't need to explicitly guarantee this.

这篇关于为什么(或不是)在构造函数中设置字段是线程安全的?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆