The Go Memory Model

[译]https://golang.google.cn/ref/mem

Go内存模型指定了一个条件,在该条件下,可以保证在一个 goroutine 中读取变量,能够获取到另一个不同 goroutine 写入同一变量产生的值。

Introduction Link to heading

Go内存模型指定了一个条件,在该条件下,可以保证在一个 goroutine 中读取变量,能够获取到另一个不同 goroutine 写入同一变量产生的值。

Advice Link to heading

如果一个程序要修改被多个 goroutine 同时访问的数据,必须序列化此类访问。

要序列化访问,请使用 channel 操作或其他同步原语(例如syncsync/atomic包中的那些)来保护数据。

如果您必须阅读本文档的其余部分以了解程序的行为,那么您就太聪明了。

别聪明。

Happens Before Link to heading

在单个 goroutine 中,读取和写入必须表现得好像它们按程序指定的顺序执行。也就是说,只有当重新排序不改变语言规范中定义的 goroutine 的行为时,编译器和处理器才可以对在单个 goroutine 中读取和写入操作的执行进行重新排序。由于这种重新排序,一个 goroutine 观察到的执行顺序可能与另一个 goroutine 感知到的顺序不同。例如,如果一个 goroutine 执行a = 1; b = 2;,另一个 goroutine 可能会在 a 的值更新之前观察到 b 的更新值。

为了指定读取和写入的要求,我们定义 发生之前(hanppen before),Go 程序中内存操作的局部顺序。如果事件 e1 在事件 e2 发生之前(hanppen before),那么我们说事件 e2 在事件 e1 发生之后(hanppen after)。另外,如果 e1 在 e2 之前没有发生并且在 e2 之后没有发生,那么我们说 e1 和 e2 同时发生。

在单个goroutine中,happens-before 顺序是程序表达的顺序。

如果以下两个都成立,则允许变量 v 的读取操作 r 观察到写入操作 w 写入到 v 的值:
1. r 没有发生在 w 之前。
2. 在 w 之后但在 r 之前没有其他写入操作 w’。

为了保证对变量 v 的读取操作 r 观察到特定写入操作 w 对 v 写入的值,确保 w 是允许读取操作 r 观察到的唯一的写入操作。也就是说,如果以下两个条件都成立,才能保证读取操作 r 能够观察到写入操作 w:
1. w 发生在 r 之前。
2. 任何其他对共享变量 v 的写入操作,要么发生在 w 之前,要么发生在 r 之后。

这组条件比第一组更加严格。它要求没有其他写入与 w 或 r 同时发生。

在单个goroutine中,没有并发,因此这两个定义是等效的:一个读取操作 r 观察最近的写入操作 w 写入 v 的值。

具有零值的 v 的类型的变量 v 的初始化表现为以上存储模型中的写入。

对于大于单个机器字的值的读取和写入操作,表现为以未指定顺序进行的多个 机器字大小的操作。

Synchronization Link to heading

Initialization Link to heading

程序的初始化在单个 goroutine 中运行,但该 goroutine 可能会创建其他并发运行的 goroutine。

如果包 p 导入包 q,则 q 的 init 函数在包 p 的任何代码开始之前完成。

函数 main.main 在所有的 init 函数完成后开始执行。

Goroutine creation Link to heading

启动新 goroutine 的 go 语句发生在该 goroutine 开始执行之前。

例如,在此程序中:

var a string

func f() {
	print(a)
}

func hello() {
	a = "hello, world"
	go f()
}

调用hello将在未来的某个时刻打印“hello,world”(也许在hello返回之后)。

Goroutine destruction Link to heading

goroutine 的退出不保证在程序中的任何事件之前发生。例如,在此程序中:

var a string

func hello() {
	go func() { a = "hello" }()
	print(a)
}

对 a 的赋值没有伴随任何同步事件,因此不保证任何其他 goroutine 都能观察到它。事实上,一个激进的编译器可能会删掉整条 go 语句。

如果一个 goroutine 影响必须被另一个 goroutine 观察到,要使用锁或 channel 通信等同步机制来建立相对顺序。

Channel communication Link to heading

channel 通信是 goroutine 之间同步的主要方法。特定 channel 上的每一个 send 操作都与该 channel 对应的 receive 操作相匹配,通常在不同的 goroutine 中。

channel 的 send 在该 channel 相应的 receive 操作完成之前发生 Link to heading

示例程序:

var c = make(chan int, 10)
var a string

func f() {
	a = "hello, world"
	c <- 0
}

func main() {
	go f()
	<-c
	print(a)
}

保证打印出 “hello, world”。对 a 的写入发生在 c 的 send 之前,即发生在 c 的相应的 receive 完成之前,即发生在print之前。

channel 的关闭发生在因通道已关闭而接收到零值返回之前 Link to heading

在前面的示例中,用 close(c)替换c <- 0会产生具有保证同样行为的程序。

无缓冲 channel 的 receive 操作在该 channel 的 send 操作完成之前发生。

示例程序(和上面一样,但是交换了 send 和 receive 语句并且使用了无缓冲的 channel):

var c = make(chan int)
var a string

func f() {
	a = "hello, world"
	<-c
}

func main() {
	go f()
	c <- 0
	print(a)
}

同样保证打印出 “hello, world”。对 a 的写入发生在 c 的 receive 之前,即发生在 c 的相应的 send 完成之前,即发生在print之前。

如果 channel 是有缓冲的,(例如,c = make(chan int, 1)),那么程序将不能保证打印 “hello, world”。(可能会打印空字符串,崩溃或执行其他操作。)

具有容量C的 channel 的第 k 次 receive 操作,在第 k+C 次 send 操作完成之前。
此规则概括了先前的有缓冲的 channel 的规则。它允许用有缓冲的 channel 建立的计数信号量:channel 中的 data 数量对应于当前的使用数量,channel 的容量对应于允许最大同时使用的数量,发送一条 data 来获取信号量,接收一条 data 来释放信号量。这是限制并发数量的常用用法。

该程序为工作列表中的每个条目启动一个 goroutine,但是 goroutine 利用limit这个 channel 来确保一次最多有三个正在运行的work函数。

var limit = make(chan int, 3)

func main() {
	for _, w := range work {
		go func(w func()) {
			limit <- 1
			w()
			<-limit
		}(w)
	}
	select{}
}

Locks Link to heading

sync包实现了两种锁的类型,sync.Mutexsync.RWMutex

对于任何sync.Mutexsync.RWMutex类型的变量l,并且n < m,第n次调用l.Unock()在第m次调用l.Lock()返回之前发生。
示例程序:

var l sync.Mutex
var a string

func f() {
	a = "hello, world"
	l.Unlock()
}

func main() {
	l.Lock()
	go f()
	l.Lock()
	print(a)
}

保证打印出"hello, world"。第一次调用l.Unlock()(在f()中),在第二次调用l.Lock()(在main()中)返回之前发生,即在print之前发生。

For any call to l.RLock on a sync.RWMutex variable l, there is an n such that the l.RLock happens (returns) after call n to l.Unlock and the matching l.RUnlock happens before call n+1 to l.Lock.

对于sync.RWMutex类型的变量ll.RLock()的任意调用,有一个 n 使得本次l.RLock()在第 n 次调用l.Unlock()之后发生(返回)并且对应的l.RUnlock()在第 n+1 调用l.Lock()之前发生。

Once Link to heading

sync包通过使用Once类型,在存在多个 goroutine 的情况下提供了一种安全的初始化机制。多个线程可以对特定的 f 执行nce.Do(f),但是只有一个线程会真正运行f(),并且其他调用会阻塞直到f()返回。

once.Do(f) 中对f()的单次调用在任意once.Do(f)的调用之前发生(返回)。
在如下程序中:

var a string
var once sync.Once

func setup() {
	a = "hello, world"
}

func doprint() {
	once.Do(setup)
	print(a)
}

func twoprint() {
	go doprint()
	go doprint()
}

调用twoprint()将只会调用setup()一次。setup 方法将在print之前完成。结果是"hello, world"将被打印两次。

Incorrect synchronization Link to heading

注意,读取操作 r 可以观察到与r同时发生的写入操作 w 所写的值。即使发生这种情况,也不意味着在 r 之后发生的读取操作将观察到在 w 之前发生的写入操作。
如下程序中:

var a, b int

func f() {
	a = 1
	b = 2
}

func g() {
	print(b)
	print(a)
}

func main() {
	go f()
	g()
}

可能会发生 g 先打印 2 然后打印 0。

这使一些常见的管用语法无效。

双重检查锁 是为了避免同步的开销。
例如,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"。

另一个不正确的惯用语法是忙着等待一个值,如:

var a string
var done bool

func setup() {
	a = "hello, world"
	done = true
}

func main() {
	go setup()
	for !done {
	}
	print(a)
}

像之前一样,不能保证在main中,观察到done的写入操作意味着同样能观察到对a的写入操作,因此这个程序也可能打印出空的字符串。更糟的是,还无法保证main能观察到对done的写入操作,因为两个线程之间没有同步事件。main中的循环无法保证能完成。

这个主题有更微妙的变体,例如这个程序:

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)
}

即使main观察到g != nil并且退出循环,无法保证它会观察到g.msg的初始化值。

在所有这些示例中,解决方案是相同的:使用显式的同步。