线程安全如何是不可变对象?

时间:2011-03-25 14:10:15

标签: .net multithreading concurrency immutability

每个人都说不可变对象是线程安全的,但为什么会这样?

在多核CPU上运行以下方案:

  • Core 1在内存位置0x100读取一个对象,并将其缓存在Core 1的L1 / L2缓存中;
  • GC会在该内存位置收集此对象,因为它已符合条件且0x100可用于新对象;
  • Core 2分配一个(不可变的)对象,该对象位于地址0x100;
  • Core 1获取对此新对象的引用,并在内存位置0x100读取它。

在这种情况下,当Core 1请求位置0x100的值时,它是否可能从其L1 / L2缓存中读取过时数据?我的直觉说这里仍然需要一个内存门来确保Core 1读取正确的数据。

上述分析是否正确,是否需要记忆门,或者我遗失了什么?

更新

我在这里描述的情况是每次GC收集时会发生的更复杂的版本。 GC收集时,会重新排序内存。这意味着对象所在的物理位置发生了变化,并且L1 / L2必须无效。大致相同的情况适用于上面的例子。

由于有理由期望.NET确保在重新排序内存后,不同的内核看到正确的内存状态,上述情况也不会成为问题。

4 个答案:

答案 0 :(得分:3)

对象的不变性不是您方案中的真正问题。相反,您的描述问题围绕着指向对象的引用,列表或其他系统。它当然需要某种技术来确保旧对象不再可用于可能试图访问它的线程。

不可变对象的线程安全性的真正意义在于不需要编写一堆代码来产生线程安全性。相反,框架,操作系统,CPU(以及其他任何东西)可以为您工作。

答案 1 :(得分:3)

我认为你要问的是,在创建一个对象之后,构造函数是否返回,并且对它的引用是否存储在某个地方,是否有可能另一个处理器上的线程仍然会看到旧数据。您提供了一种方案,即保存对象的实例数据的高速缓存行以前用于其他目的。

在特别弱的内存模型下,这样的事情可能是可能的,但我希望任何有用的内存模型,即使是相对较弱的内存模型,也能确保解除引用不可变对象是安全的,即使这样的安全性需要填充对象足以在对象实例之间不共享高速缓存行(GC几乎肯定会在完成时使所有高速缓存无效,但是如果没有这样的填充,核心#2创建的不可变对象可能与一个对象共享一个高速缓存行。核心#1之前曾读过)。如果没有这种安全级别,编写健壮的代码将需要如此多的锁和内存障碍,以至于编写多处理器代码并不比单处理器代码慢。

流行的x86和x64内存型号提供了您所寻求的保证,并且更进一步。处理器协调缓存行的“所有权”;如果多个处理器想要读取相同的缓存行,他们可以毫无阻碍地这样做。当处理器想要写入缓存行时,它会与其他处理器协商以获得所有权。获得所有权后,处理器将执行写入。在拥有缓存行的处理器放弃之前,其他处理器将无法读取或写入缓存行。请注意,如果多个处理器想要同时写入相同的缓存行,他们可能会花费大部分时间来协商缓存行所有权而不是执行实际工作,但会保留语义正确性。

答案 2 :(得分:1)

你错过了它确实是一个糟糕的垃圾收集器让这件事情发生了。对核心1的引用应该阻止该对象成为GCd。

答案 3 :(得分:1)

我不确定内存门会改变这种情况,因为这肯定只影响后续读取......然后问题变成从何处读取?如果它来自一个字段(必须至少是静态的,或某个实例的实例字段仍然在堆栈上或以其他方式可达)或本地变量 - 那么根据定义它不可用于集合。

现在在寄存器中,该引用仅 的情况......这是非常棘手的。直觉上我想说“不,这不是问题”,但需要详细研究内存模型才能证明这一点。但处理引用是这样的一个简单的常见场景:这必须起作用。