英文:
How is this code snippet an example of incorrect synchronization?
问题
我正在尝试理解《Go内存模型》中有错误同步代码的示例。
双重检查锁定是为了避免同步开销。例如,twoprint程序可能被错误地编写为:
var a string
var done bool
func setup() {
a = "hello, world"
done = true
}
func doprint() {
if !done {
once.Do(setup)
}
print(a)
}
func twoprint() {
go doprint()
go doprint()
}
但是不能保证在doprint
中观察到对done的写入也意味着观察到对a
的写入。这个版本可能(错误地)打印一个空字符串而不是"hello, world"
。
为什么会打印一个空字符串而不是"hello, world"
?我运行了这段代码大约五次,每次都打印出了"hello, world"
。编译器会交换a = "hello, world"
和done = true
这两行代码进行优化吗?只有在这种情况下,我才能理解为什么会打印一个空字符串。
非常感谢!下面是修改后的测试代码:
package main
import (
"fmt"
"sync"
)
var a string
var done bool
var on sync.Once
func setup() {
a = "hello, world"
done = true
}
func doprint() {
if !done {
on.Do(setup)
}
fmt.Println(a)
}
func main() {
go doprint()
go doprint()
select {}
}
英文:
I am trying to understand the example with incorrect sync code from The Go Memory Model.
> Double-checked locking is an attempt to avoid the overhead of synchronization. For example, the twoprint program might be incorrectly written as:
var a string
var done bool
func setup() {
a = "hello, world"
done = true
}
func doprint() {
if !done {
once.Do(setup)
}
print(a)
}
func twoprint() {
go doprint()
go doprint()
}
> but there is no guarantee that, in doprint
, observing the write to done implies observing the write to a
. This version can (incorrectly) print an empty string instead of "hello, world"
.
What are the detailed reasons for an empty string printed in place of "hello world"? I ran this code about five times, and every time, it printed "hello world".
Would the compiler swap a line a = "hello, world"
and done = true
for optimization? Only in this case, I can understand why an empty string would be printed.
Thanks a lot!
At the bottom, I've attached the changed code for the test.
package main
import(
"fmt"
"sync"
)
var a string
var done bool
var on sync.Once
func setup() {
a = "hello, world"
done = true
}
func doprint() {
if !done {
on.Do(setup)
}
fmt.Println(a)
}
func main() {
go doprint()
go doprint()
select{}
}
答案1
得分: 4
根据Go内存模型:
除非两个goroutine之间存在显式的同步(使用通道、互斥锁等),否则不能保证一个goroutine能够看到另一个goroutine执行的操作。
在你的例子中,一个goroutine看到done=true
并不意味着它会看到a
的设置。只有在goroutine之间存在显式的同步时,才能保证这一点。
sync.Once
可能提供了这样的同步,这就是为什么你没有观察到这种行为的原因。仍然存在内存竞争,并且在具有不同sync.Once
实现的不同平台上,情况可能会发生变化。
英文:
According to the Go memory model:
There are no guarantees that one goroutine will see the operations performed by another goroutine unless there is an explicit synchronization between the two using channels, mutex. etc.
In your example: the fact that a goroutines sees done=true
does not imply it will see a
set. This is only guaranteed if there is explicit synchronization between the goroutines.
The sync.Once
probably offers such synchronization, so that's why you have not observed this behavior. There is still a memory race, and on a different platform with a different implementation of sync.Once
, things may change.
答案2
得分: 2
关于Go内存模型的参考页面告诉你以下内容:
编译器和处理器只有在重新排序不改变语言规范定义的goroutine内部行为时,才能重新排序单个goroutine内执行的读写操作。
因此,编译器可以重新排序setup
函数体内的两个写操作,从
a = "hello, world"
done = true
到
done = true
a = "hello, world"
然后可能出现以下情况:
- 一个
doprint
goroutine没有观察到对done
的写操作,因此启动了一次setup
函数的单次执行; - 另一个
doPrint
goroutine观察到对done
的写操作,但在观察到对a
的写操作之前就执行完毕了,因此打印出了a
类型的零值,即空字符串。
我运行了这段代码大约五次,每次都打印出了"hello world"。
你需要理解“同步错误”(代码的属性)和“竞态条件”(特定执行的属性)之间的区别;Valentin Deleplace的这篇文章在阐明这一区别方面做得很好。简而言之,同步错误可能会导致竞态条件的出现,但仅仅因为竞态条件在程序的多次执行中没有显现,并不意味着你的程序没有错误。
在这里,你可以通过重新排序setup
中的两个写操作并在两者之间添加一个微小的延迟来“强制”出现竞态条件。
func setup() {
done = true
time.Sleep(1 * time.Millisecond)
a = "hello, world"
}
这可能足以让你相信该程序确实包含一个同步错误。
英文:
The reference page about the Go Memory Model tells you the following:
> 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.
The compiler may therefore reorder the two writes inside the body of the setup
function, from
a = "hello, world"
done = true
to
done = true
a = "hello, world"
The following situation may then occur:
- One
doprint
goroutine doesn't observe the write todone
and therefore initiates a single execution of thesetup
function; - The other
doPrint
goroutine observes the write todone
but finishes executing before observing the write toa
; it therefore prints the zero value ofa
's type, i.e. the empty string.
> I ran this code about five times, and every time, it printed "hello world".
You need to understand the distinction between a synchronization bug (a property of the code) and a race condition (a property of a particular execution); this post by Valentin Deleplace does a great job at elucidating that distinction. In short, a synchronization bug may or may not give rise to a race condition; however, just because a race condition doesn't manifest itself in a number of executions of your program doesn't mean your program is bug-free.
Here, you can "force" the race condition to occur simply by reordering the two writes in setup
and adding a tiny sleep between the two.
func setup() {
done = true
time.Sleep(1 * time.Millisecond)
a = "hello, world"
}
This may be enough to convince you that the program indeed contains a synchronization bug.
答案3
得分: 0
程序不是内存安全的原因是:
- 多个 goroutine 同时访问相同的内存(
done
和a
)。 - 并发访问没有始终受到显式同步的控制。
- 这些访问可能会写入/修改内存。
试图推断程序在这些变量方面的行为是不必要的混乱,因为它实际上是未定义的行为。没有“正确”的答案。只有偶然的观察,它们没有硬性保证何时成立。
英文:
The program is not memory safe because:
- Multiple goroutines concurrently access the same memory (
done
anda
). - The concurrent access is not always controlled by explicit synchronization.
- The accesses may write to / modify the memory.
Trying to reason about how the program will or will not behave with regard to these variables is probably just unneeded confusion, because it's literally undefined behaviour. There is no "correct" answer. Only circumstantial observations, which have no hard guarantee of if or when they hold true.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论