让我们说你有一个像这样的简单课:
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是一种无法原子编写的类型(例如自定义结构)会不会很重要?
答案 0 :(得分:11)
编写的代码将从CLR2.0开始工作,因为CLR2.0内存模型保证所有商店都具有发布语义。
发布语义:确保围栏前没有加载或存储 围栏后会移动。之后的说明可能仍然发生在之前 围栏。(取自CPOW页面512)。
这意味着在分配类引用后无法移动构造函数初始化。
Joe duffy在他的article about the very same subject中提到了这一点。
规则2:所有商店都有发布语义,即没有加载或存储可能 一个接一个地移动。
Vance morrison的article here也证实了这一点(章节技巧4:懒惰的初始化)。
与删除读锁的所有技术一样,图7中的代码 依赖于强大的写入顺序。例如,这段代码就是 ECMA内存模型不正确,除非myValue变得易失 因为初始化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允许)。情况也是如此 由非Microsoft开发人员编写的目标.NET的大多数代码 定位Windows
结果是框架中的很多代码在运行时都破了 IA64,特别是与臭名昭着的双重检查的代码 锁定模式,突然无法正常工作。我们来看看这个 在本章后面的模式中。但总的来说, 如果商店可以通过其他商店,请考虑:一个线程可能 初始化私有对象的字段,然后发布对它的引用 它在一个共享的位置;因为商店可以四处走动,另一个 线程可能能够看到对象的引用,读取它,和 然而,当他们仍然是一个未初始化的状态时,看到领域。 这不仅影响现有代码,还可能违反类型系统 属性,如initonly fields。
因此CLR架构师决定通过发射强化2.0 IA64上的所有商店都作为释放围栏。这给了所有CLR程序 更强的记忆模型行为。这确保了程序员不需要 不得不担心只会出现的微妙的竞争条件 在一个不起眼的,很少使用和昂贵的架构上练习。
注意Joe duffy表示他们通过在IA64上发布所有商店作为发布围栏来强化2.0 这并不意味着其他处理器可以重新排序。其他处理器本身固有地保证商店(商店后面的商店)不会被重新排序。所以CLR不需要明确保证这一点。
答案 1 :(得分:1)
上述代码是线程安全的。构造函数在被分配给"值"之前完全执行。变量。第二个循环中的本地副本将为null或完全构造的实例,因为分配实例引用是内存中的原子操作。
如果"价值"是一个结构然后它不会是线程安全的,因为值的初始化不是原子的。
答案 2 :(得分:1)
因此,似乎这可能会失败,因为写入值字段可能会在写入a和b之前发生。这可能吗
是的,这肯定是可能的。
您需要以某种方式同步对数据的访问,以防止此类重新排序。
答案 3 :(得分:0)
我会看到这个(或其他类似的多线程代码)抛出异常吗?
是,在ARM(以及任何其他具有弱内存模型的硬件)上,您将观察到此类行为。
我经常看到非易失性写入可能没有的事实 其他线程立即看到。因此,似乎可以这样 失败,因为写入值字段可能发生在 写到a和b。这是可能的,还是有什么东西在 内存模型使这个(非常常见)模式安全吗?
易失性不是关于观察变化的瞬时性,而是关于秩序和获取/释放语义。
此外,ECMA-335表示它可能发生(并且它将发生在ARM或任何其他具有弱内存模型的硬件上)。
为此目的,readonly是否重要?
readonly
与指令重新排序和易失性无关。
如果a和b是一种无法原子编写的类型(例如自定义结构),这是否重要?
在这种情况下,字段的原子性并不重要。
为了防止这种情况,您应该通过Volatile.Write
编写对创建对象的引用(或者只是使该引用volatile
,编译器将完成这项工作)。 Volatile.Write(ref value, new MyClass(1, 1))
会做到这一点。
有关易失性语义和内存模型的更多信息,请参阅ECMA-335,第I.12.6节
答案 4 :(得分:0)
如上所述,此代码是线程安全的,因为value
在构造函数执行完毕之前不会更新。换句话说,其他任何人都没有观察到正在建造的物体。
您可以编写代码,通过明确将this
发布到外部世界来帮助您拍摄自己的内容,例如
class C
{
public C( ICObserver observer )
{
observer.Observe(this);
}
}
当Observe()执行时,所有的赌注都会被关闭,因为它不再适用于外界没有观察到该对象。
答案 5 :(得分:-2)
这是错的,对不起......
如果您的测试在之前执行,我认为您可能会抛出错误 在另一个线程中首先分配变量。这将是一个 竞争条件......这可能是一个间歇性的问题。
如果while循环检查的值,您也可能会收到错误 确定新类被实例化并赋值的确切时刻, 但在设置a和b变量之前。
至于更好的方法,这取决于你的目标。有没有理由值被覆盖?我认为将新类放入需要按顺序处理的集合中更为常见。
有些集合可以解决这个问题。您可以在一个线程中向集合添加类,并在另一个线程中检查并提取它们。
有关示例,请参阅http://dotnetcodr.com/2014/01/14/thread-safe-collections-in-net-concurrentstack/。