英文:
Question on the go memory model,the last example
问题
我对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 的值?我想详细了解原因。
英文:
i have a question on the go memory model.
in the last example:
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)
}
In my opnion,reads and writes of values with a single machine word is a atomic behavior.I try many times to run the test but it is always can be observed.
So please tell me why g.msg is not guarntee to observed? I want to know the reason in detail,please.
答案1
得分: 1
因为在启动的 goroutine 中有两个写操作:
t := new(T) // 第一个
t.msg = "hello, world" // 第二个
g = t
可能是因为在最后一行中,main
goroutine 可能会观察到对 g
的非 nil
指针赋值,但由于两个 goroutine 之间没有明确的同步,编译器允许重新排序操作(这不会改变启动的 goroutine 中的行为),例如:
t := new(T) // 第一个
g = t
t.msg = "hello, world" // 第二个
如果操作被重新排列成这样,启动的 goroutine (setup()
) 的行为将不会改变,因此编译器是允许这样做的。在这种情况下,main
goroutine 可能会观察到 g = t
的效果,但不会观察到 t.msg = "hello, world"
。
为什么编译器会重新排序操作呢?例如,因为不同的顺序可能会导致更高效的代码。例如,如果分配给 t
的指针已经在寄存器中,那么它也可以立即分配给 g
,而不必在分配给 g
之前重新加载它。
这在Happens Before部分中提到:
在单个 goroutine 中,读取和写入必须表现得好像它们按程序指定的顺序执行。也就是说,编译器和处理器只有在重新排序不会改变该 goroutine 内部行为(由语言规范定义)时,才能重新排序在单个 goroutine 中执行的读取和写入。由于这种重新排序,一个 goroutine 观察到的执行顺序可能与另一个 goroutine 感知到的顺序不同。例如,如果一个 goroutine 执行
a = 1; b = 2;
,另一个 goroutine 可能在更新的a
值之前观察到更新的b
值。
如果使用适当的同步,将禁止编译器执行可能改变其他 goroutine 观察到的行为的重新排列。
无论运行你的示例多少次并且没有观察到这一点都没有意义。问题可能永远不会出现,可能会在不同的架构上出现,或者在不同的机器上出现,或者在使用不同(未来)版本的 Go 编译时出现。不要依赖于不被保证的行为。始终使用适当的同步,在应用程序中不要留下任何数据竞争。
英文:
Because there are 2 write operations in the launched goroutine:
t := new(T) // One
t.msg = "hello, world" // Two
g = t
It may be that the main
goroutine will observe the non-nil
pointer assignment to g
in the last line, but since there is no explicit synchronization between the 2 goroutines, the compiler is allowed to reorder the operations (that doesn't change the behavior in the launched goroutine), e.g. to the following:
t := new(T) // One
g = t
t.msg = "hello, world" // Two
If operations would be rearranged like this, the behavior of the launched goroutine (setup()
) would not change, so a compiler is allowed to to this. And in this case the main
goroutine could observe the effect of g = t
, but not t.msg = "hello, world"
.
<sup>Why would a compiler reorder the operations? E.g. because a different order may result in a more efficient code. E.g. if the pointer assigned to t
is already in a register, it can also be assigned to g
right away, without having to reload it again if the assignment to g
would not be executed right away.</sup>
This is mentioned in the Happens Before section:
> Within a single goroutine, reads and writes must behave as if they executed in the order specified by the program. That is, compilers and processors may reorder the reads and writes executed within a single goroutine only when the reordering does not change the behavior within that goroutine as defined by the language specification. Because of this reordering, the execution order observed by one goroutine may differ from the order perceived by another. For example, if one goroutine executes a = 1; b = 2;
, another might observe the updated value of b
before the updated value of a
.
If you use proper synchronization, that will forbid the compiler to perform such rearranging that would change the observed behavior from other goroutines.
Running your example any number of times and not observing this does not mean anything. It may be the problem will never arise, it may be it will arise on a different architecture, or on a different machine, or when compiled with a different (future) version of Go. Simply do not rely on such behavior that is not guaranteed. Always use proper synchronization, never leave any data races in your app.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论