此代码片段如何成为不正确同步的示例?

How is this code snippet an example of incorrect synchronization?

我正在尝试理解 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".

打印空字符串代替“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 集。这只有在 goroutine 之间存在显式同步时才能保证。

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

reference page about the Go Memory Model 告诉您以下内容:

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.

因此,编译器可能会重新排序 setup 函数体内的两次写入,来自

a = "hello, world"
done = true

done = true
a = "hello, world"

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

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

I ran this code about five times, and every time, it printed "hello world".

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

在这里,您可以通过重新排序 setup 中的两个写入并在两者之间添加一个小的休眠来“强制”出现竞争条件。

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

(Playground)

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

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

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

试图推断程序如何根据这些变量运行或不运行可能只是不必要的混淆,因为它实际上是未定义的行为。没有“正确”的答案。只有间接观察,无法保证它们是否或何时成立。