此代码片段如何成为错误同步的示例?



我正在尝试理解来自 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中,观察写入完成意味着观察写入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{}
}

根据 Go 内存模型:

https://golang.org/ref/mem

不能保证一个 goroutine 会看到另一个 goroutine 执行的操作,除非两者之间使用通道、互斥锁等进行显式同步。

在你的例子中:goroutines 看到done=true并不意味着它会看到a集合。仅当 goroutines 之间存在显式同步时,才能保证这一点。

sync.Once可能提供这种同步,所以这就是你没有观察到这种行为的原因。仍然存在内存竞赛,并且在具有不同sync.Once实现的不同平台上,事情可能会发生变化。

关于 Go 内存模型的参考页面告诉您以下内容:

编译器和处理器可以对单个 GoRoutine 中执行的读取和写入进行重新排序,前提是重新排序不会更改语言规范定义的 GoRoutine 中的行为。

因此,编译器可以在setup函数主体内的两次写入重新排序,从

a = "hello, world"
done = true

done = true
a = "hello, world"

然后可能会出现以下情况:

  • 一个doprintgoroutine 不观察对done的写入,因此启动setup函数的单次执行;
  • 另一个doPrintgoroutine观察对done的写入,但在观察对a的写入之前完成执行;因此它打印a类型的零值,即空字符串。

我运行了大约五次这个代码,每次,它都打印"hello world"。

你需要了解同步错误(代码的属性)和竞争条件(特定执行的属性)之间的区别;Valentin Deleplace的这篇文章在阐明这种区别方面做得很好。简而言之,同步错误可能会也可能不会引起争用条件;但是,仅仅因为争用条件没有在程序的多次执行中表现出来,并不意味着程序没有错误。

在这里,您可以通过对setup中的两个写入重新排序并在两者之间添加微小的睡眠来"强制"发生争用条件。

func setup() {
done = true
time.Sleep(1 * time.Millisecond)
a = "hello, world"
}

(游乐场)

这可能足以让您相信该程序确实包含同步错误。

该程序不是内存安全的,因为:

  • 多个goroutine同时访问同一个内存(donea)。
  • 并发访问并不总是由显式同步控制。
  • 访问可能会写入/修改内存。

试图推理程序在这些变量上的行为可能只是不必要的混乱,因为它实际上是未定义的行为。没有"正确"的答案。只有间接观察,不能硬性地保证它们是否或何时成立。

最新更新