我有一个关于go内存模型的问题。在最后一个例子中:
type T struct {
msg string
}
var g *T
func setup() {
t := new(T)
t.msg = "hello, world"
g = t
}
func main() {
go setup()
for g == nil {
}
print(g.msg)
}
在我因为,读取和写入的值和一个机器字是一个原子的行为。我试了很多次运行测试,但它总是可以被观察到。所以请告诉我为什么g.msg不能保证被观察到?我想知道详细的原因。
因为在启动的例程中有2个写操作:
t := new(T) // One
t.msg = "hello, world" // Two
g = t
可能main
的运行例程会观察到最后一行g
的非nil
指针赋值,但由于两个运行例程之间没有显式的同步,编译器被允许重新排序操作(这不会改变启动的运行例程的行为),例如:
t := new(T) // One
g = t
t.msg = "hello, world" // Two
如果操作像这样重新排列,启动的程序(setup()
)的行为不会改变,所以编译器允许这样做。在这种情况下,main
例程可以观察到g = t
的效果,但不能观察到t.msg = "hello, world"
。
为什么编译器要重新排序操作?例如,因为不同的顺序可能会产生更有效的代码。例如,如果分配给t
的指针已经在寄存器中,它也可以立即分配给g
,如果分配给g
的指针不会立即执行,则不必再次重新加载它。
这在Happens Before一节中提到:
在单个例程中,读和写必须按照程序指定的顺序执行。也就是说,编译器和处理器只有在重新排序不会改变语言规范所定义的该程序中的行为时,才可以对单个程序中执行的读和写操作进行重新排序。由于这种重新排序,一个程序观察到的执行顺序可能与另一个程序感知到的顺序不同。例如,如果一个线程执行
a = 1; b = 2;
,另一个线程可能会在a
的更新值之前观察到b
的更新值。
如果你使用适当的同步,那将禁止编译器执行这样的重新排列,这将改变从其他例程观察到的行为。
运行你的例子任意次数而没有观察到这一点并不意味着什么。这个问题可能永远不会出现,它可能会出现在不同的架构上,或者在不同的机器上,或者在使用不同(未来)版本的Go编译时出现。简单地说,不要依赖这种没有保证的行为。始终使用适当的同步,永远不要在应用程序中留下任何数据竞争。