当引用字段时,是否需要内存屏障(.net x86或x64)?

3
在下面这段代码中,如果Proc1和Proc2在不同的处理器上同时执行,ThingVal2是否可能得到一个除5以外的值(例如0)?
Class SimpleThing
    Public X As Integer
    Sub New(ByVal value As Integer)
        X = value
    End Sub
End Class
Class ConcurrencyTest
    Dim Thing1 As New SimpleThing(5)
    Dim Thing2 As New SimpleThing(0)
    Dim ThingRef As SimpleThing = Thing1
    Dim ThingVal1, ThingVal2 As Integer
    Sub Proc1()
        Thing2.X = 5
        Threading.Thread.MemoryBarrier()
        ThingRef = Thing2
    End Sub
    Sub Proc2()
        ThingVal1 = Thing2.X
        ThingVal2 = ThingRef.X
    End Sub
End Class
我知道在像IA64这样的弱模型中,Proc2看到的ThingRef可能已经改变,但是没有看到Thing2的X字段已经改变。那么在x86或x64上运行的.NET应用程序是否存在此风险?如果Proc1创建了一个新的SimpleThing实例,将其X字段设置为5,然后将ThingRef设置为指向它,那么这是否足以避免危险,还是可能会分配新的对象与Proc2线程访问的其他内容共享缓存行?
多线程代码的常见范式是构建一个不可变对象并设置可变引用指向它(可能使用Interlocked.CompareExchange)。在x86 / x64下,无论线程如何,读取不可变类型是否总是安全的,还是可能会出现问题?如果是后者,那么在vb.net中保证可靠行为的首选方式是什么?
此外,是否有任何方法可以指定代码必须以无法发生这种问题的方式运行(例如,在像IA64这样的处理器上将执行限制为单个核心,否则不能保证正确操作)?

只有在一个周期内可以读取值时,才能保证不可变。int和bool属于这个类别。对于long来说就不太确定了,而string则绝对不是。 - SRM
@SRM:不可变对象是一旦向任何其他人提供了引用,就永远不会更改的对象。上面的“SimpleThing”对象不是不可变的,因为我试图准确地询问在.NET x86和.NET x64内存模型下保证什么和不保证什么。 - supercat
1个回答

0

好的,你提出了很多问题。我会尽力回答我所知道的。

-1. 关于你的代码示例:

CLR 2.0以后具备有序存储功能。这意味着在x86/x64架构上,你的ThingVal永远都是5。可以确定。虽然我没有在真正的IA64上试过,但它应该也能正常工作,因为CLR应该确保在所有平台上进行有序写入,并且对于你的简单示例来说应该足够了。

-2. IA64和x86/x64的区别:

x86/x64具有不同的内存语义,不存在IA64中的这种风险。这里唯一可能存在的问题是,你实际上正在使用一种更高级的语言,如果它使用了优化编译器(像C++一样),那么不了解编译器如何进行优化的话,就无法预测任何事情。未记录下来的是:VB不执行任何全局优化等,所以你的代码应该是安全的。

-3. 关于不可变性:

如果你只是读取它,并且它确实是不可变的,那就是安全的。

-4. 关于单核:

你可以设置线程的亲和性。这是每个线程的标准属性,用于定义线程可以在哪些CPU上运行。(.NET中的线程亲和性设置直接修改操作系统中的亲和性。)但这会使你的程序运行得很慢。

此外,您可以切换到C#并使用volatile关键字。它将帮助您更轻松地生活,因为它使对易失变量的任何更改立即被所有CPU看到,从而解决了您在此处提出的所有可能问题。不幸的是,VB不提供此关键字。


我知道x86有有序存储,但这里的问题在于负载依赖性。Instance2的Field X可能在写入Instance2.X或ThingRef之前就已经被Proc2读取了。在.NET下,当代码读取ThingRef.X时,是否有任何保证它不会简单地使用从Instance2.X读取的缓存值?这是一个我没有听说过讨论的边角情况。 - supercat
我知道可以指定某些线程只能在例如第五核上运行,但这似乎过于具体。真正需要的是一种指定两个或多个线程不能同时在不同处理器上运行的方法,并且每当执行从一个处理器移动到另一个处理器时,旧处理器和新处理器必须按照顺序刷新它们的缓存。 - supercat
是的,“有序存储”确保当您读取ThingRef.X时,您要么读取新的ThingRef和新的X,要么读取旧的ThingRef和旧的X。这是因为您的X在ThingRef内部,所以您总是先读取ThingRef再读取X。所以它是安全的。即使使用有序存储,如果您在同一对象中具有X1和X2并且它们彼此相关,则可能会不安全。 - Al Kepp
@Al Kepp:有没有明确的文件说明对象实例内的字段保证在对象引用之后被加载,即使该实例字段刚刚通过另一个对象引用进行了访问?哪种手段可以强制执行这种保证?全面的内存障碍是昂贵的(并且很可能在未来变得更加昂贵),但我不知道如何指定更窄的约束。评估foo.x + bar.y最多应该需要一个屏障(获取foo,获取bar,屏障,获取fetchedfoo.x和fetchedbar.y),但我不知道编译器是否能够做到这一点。 - supercat

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接