[英]Why is (or isn't) setting fields in a constructor thread-safe?
假设你有一个像这样的简单类:
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);
}
我的问题是:我会看到这个(或其他类似的多线程代码)抛出异常吗? 我经常看到其他线程可能不会立即看到非易失性写入的事实。 因此,似乎这可能会失败,因为写入值字段可能会在写入a和b之前发生。 这是可能的,或者内存模型中是否存在使这种(非常常见)模式安全的东西? 如果是这样,它是什么? 为了这个目的,readonly是否重要? 如果a和b是一个无法原子编写的类型(例如自定义结构),这是否重要?
编写的代码将从CLR2.0开始工作,因为CLR2.0内存模型保证所有商店都具有发布语义 。
释放语义:确保在栅栏后栅栏移动之前没有载荷或存储。 之后的说明可能仍然发生在围栏之前。(取自CPOW第512页)。
这意味着在分配类引用后无法移动构造函数初始化。
Joe duffy在他关于同一主题的文章中提到了这一点。
规则2:所有商店都有发布语义,即没有加载或商店可能会在一个商店之后移动。
此外,Vance morrison的文章也证实了这一点(章节技巧4:懒惰初始化)。
与删除读锁的所有技术一样,图7中的代码依赖于强写入顺序。 例如,此代码在ECMA内存模型中是不正确的,除非myValue变为volatile,因为初始化LazyInitClass实例的写入可能会延迟到写入myValue之后,允许GetValue的客户端读取未初始化状态。 在.NET Framework 2.0模型中,代码在没有volatile声明的情况下工作。
保证从CLR 2.0开始按顺序写入。 它没有在ECMA标准中指定,它只是CLR的微软实现给出了这种保证。 如果您在CLR 1.0或CLR的任何其他实现中运行此代码, 您的代码可能会中断 。
这一变化背后的故事是:(来自CPOW Page 516)
当CLR 2.0被移植到IA64时,它的初始开发发生在X86处理器上,因此它很难处理任意存储重新排序(如IA64所允许的)。 大多数针对Windows的非Microsoft开发人员编写的目标.NET代码也是如此
结果是在IA64上运行时框架中的许多代码都破了,特别是与臭名昭着的双重检查锁定模式有关的代码突然无法正常工作。 我们将在本章后面的模式环境中对此进行研究。 但总的来说,如果商店可以通过其他商店,请考虑这一点:一个线程可能初始化私有对象的字段,然后在共享位置发布对它的引用; 因为商店可以四处移动,另一个线程可能能够看到对象的引用,读取它,然后在它们仍处于未初始化状态时看到字段。 这不仅影响现有代码,还可能违反类型系统属性,例如initonly字段。
因此,CLR架构师决定通过将IA64上的所有商店作为发布围栏来增强2.0。 这为所有CLR程序提供了更强大的内存模型行为。 这可以确保程序员不必担心细微的竞争条件,这些条件只会在一个模糊,很少使用和昂贵的架构上体现出来。
注意Joe duffy说他们通过在IA64上发布所有商店作为释放围栏 来 强化2.0, 这并不意味着其他处理器可以重新排序它 。 其他处理器本身固有地保证商店(商店后面的商店)不会被重新排序。 所以CLR不需要明确保证这一点。
上述代码是线程安全的。 构造函数在分配给“value”变量之前完全执行。 第二个循环中的本地副本将为null或完全构造的实例,因为分配实例引用是内存中的原子操作。
如果“value”是一个结构,那么它将不是线程安全的,因为值的初始化不是原子的。
因此,似乎这可能会失败,因为写入值字段可能会在写入a和b之前发生。 这可能吗
是的,这肯定是可能的。
您需要以某种方式同步对数据的访问以防止这种重新排序。
我会看到这个(或其他类似的多线程代码)抛出异常吗?
是的 ,在ARM(以及具有弱内存模型的任何其他硬件)上,您将观察到此类行为。
我经常看到其他线程可能不会立即看到非易失性写入的事实。 因此,似乎这可能会失败,因为写入值字段可能会在写入a和b之前发生。 这是可能的,或者内存模型中是否存在使这种(非常常见)模式安全的东西?
易失性不是关于观察变化的瞬时性,而是关于秩序和获取/释放语义。
此外,ECMA-335表示它可能发生(并且它将发生在ARM或具有弱内存模型的任何其他硬件上)。
为了这个目的,readonly是否重要?
readonly
与指令重新排序和volatile无关。
如果a和b是一个无法原子编写的类型(例如自定义结构),这是否重要?
在这种情况下,字段的原子性无关紧要。 为了防止这种情况,你应该通过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()执行时,所有的赌注都会被关闭,因为它不再适用于外界没有观察到的对象。
这是错的,对不起......
我认为如果你的测试是在另一个线程中第一次赋值变量之前执行的话,你可能会抛出错误。 这将是一场竞争条件......这可能是一个间歇性的问题。
如果while循环在实例化新类并分配给value的确切时刻检查值,但在设置a和b变量之前,也可能会出错。
至于更好的方法,这取决于你的目标。 有没有理由值被覆盖? 我认为将新类放入需要按顺序处理的集合中更为常见。
有些集合可以解决这个问题。 您可以在一个线程中向集合添加类,并在另一个线程中检查并提取它们。
有关示例,请参见http://dotnetcodr.com/2014/01/14/thread-safe-collections-in-net-concurrentstack/ 。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.