# Go 内存模型 [原文地址](https://golang.google.cn/ref/mem) ## 简介(Introduction) **Go 内存模型**涉及到两个 Go协程 之间对同一个变量的读写。假如有一个变量,其中一个 Go协程(a) 写这个变量,另一个 Go协程(b) 读这个变量;Go 内存模型定义了**什么情况下** Go协程(b) 能够确保读取到由 Go协程(a) 写入的值。 ## 建议(Advice) 多协程并发修改数据的程序必须保证各个步骤串行执行。 为了串行执行各步,可以使用**信道**或 [sync](https://golang.google.cn/pkg/sync/) 和 [sync/atomic](https://golang.google.cn/pkg/sync/atomic/) 两个包里的同步原语来保护被共享的数据。 因为并发编程是一件很复杂的事情,请大家在学习本文的基础上再多加练习,不能仅依赖本文来掌握并发编程的知识。 ## 发生在...之前(Happens Before) 在一个 Go协程 里,对同一个变量的读写必然是按照代码编写的顺序来执行的。对于多个变量的读写,如果重新排序不影响代码逻辑的正常执行,编译器和处理器可能会对多个变量的读写过程**重新排序**;比如对于 `a = 1; b = 2` 这两个语句,在同一个 Go协程 里先执行 `a=1` 还是先执行 `b=2` 其实是没有区别的。但是,因为重新排列执行顺序的情况的存在,会导致某个 Go协程 所观察到的执行顺序可能与另一个 Go协程 观察到的执行顺序不一样。还是拿 `a = 1; b = 2` 举例,如果在某个协程里依次执行 `a = 1; b = 2`,由于**重新排序**的存在可能另一个 Go协程 观察到的事实是 `b` 的值先被更新,而 `a` 的值被后更新。 为了表征读写需求,我们可以定义“发生在...之前”,用来表示 Go 语言中某一小段内存命令的执行顺序。如果事件 `e1` 发生在事件 `e2` 之前,此时我们就认为 `e2` 发生在 `e1` 之后。如果事件 `e1` 既不发生在事件 `e2` 之前,也不发生在 `e2` 之后,此时我们就认为 `e1` 和 `e2` 同时发生(并发)。 在一个 Go协程 内部,谁发生在谁之前的顺序就是代码显式定义的顺序。比如: ```go a := 1 fmt.Println(a) ``` 肯定是 `a := 1` 先执行,`fmt.Println(a)` 后执行。 当 Go协程 不仅仅局限在一个的时候,存在下面两个规则: **规则一**:如果存在一个变量 v,如果下面的两个条件都满足,则读操作 `r` **允许观察到**(可能观察到,也可能观察不到)写操作 `w` 写入的值: 1. `r` **不在** `w` 之前发生; 2. 不存在其他的 `w’` 在 `w` 之后发生,也不存在 `w’` 在 `r` 之前发生。 **规则二**:为了保证读操作 `r` 读取到的是写操作 `w` 写入的值,需要确保 `w` 是唯一允许被 `r` 观察到的写操作。如果下面的两个条件都满足,则 `r` 保证能够观察到 `w` 写入的值: 1. `w` 发生在 `r` 之前; 2. 其他对共享变量 `v` 的写操作要么发生在 `w` 之前,要么发生在 `r` 之后。 规则二的条件比规则一的条件更为严格,它要求没有其他的写操作和 `w`、`r` 并发地发生。 在一个 Go协程 里是不存在并发的,因此规则一和规则二是等效的:读操作 `r` 可以观察到最近一次写操作 `w` 写入的值。但是,当多个协程访问一个共享变量的时候,就必须**使用同步事件来构建**“发生在...之前”的条件,从而保证读操作观察到的一定是想要的写操作。 在内存模型中,变量 `v` 的零值初始化操作等同于一个写操作。 如果变量的值大于**单机器字**(CPU 从内存单次读取的字节数),那么 CPU 在读和写这个变量的时候是以一种不可预知顺序的多次执行**单机器字**的操作,这也是 [sync/atomic](https://golang.google.cn/pkg/sync/atomic/) 包存在的价值。 ## 同步 ### 初始化 程序的初始化是在一个单独的 Go协程 中进行的,但是这个协程可以创建其他的 Go协程 并且二者并发执行。 * 如果一个包 `p` 导入了包 `q`, 那么 `q` 的 `init` 函数的执行**发生在** `p`的所有 `init` 函数的执行**之前**。 * 函数 `main.main` 的执行**发生在**所有的 `init` 函数执行完成**之后**。 ### Go协程的创建 在 Go 语言中通过 `go` 语句创建新的 Go协程 发生在这个 Go协程 的执行**之前**。比如下面的例子: ```go var a string func f() { print(a) } func hello() { a = "hello, world" go f() } ``` 调用函数 `hello` 会在调用后的某个时间点打印 “hello, world” ,这个时间点可能在 `hello` 函数返回之前,也可能在 `hello` 函数返回之后。 ### Go协程的销毁 Go协程的退出无法确保**发生在**程序的某个事件**之前**。比如下面的例子: ```go var a string func hello() { go func() { a = "hello" }() print(a) } ``` 其中 `a` 的赋值语句没有任何的同步事件,因此无法保证被其他任意的 Go 协程观察到这个赋值事件的存在。事实上,一些编译器可能会在编译阶段删除上面代码中的整个 `go` 语句。 如果某个 Go协程 里发生的事件必然要被另一个 Go协程 观察到,需要使用同步机制进行保证,比如使用**锁**或者**信道**(channel)通信来构建一个相对的事件发生顺序。 ### 信道通信 信道通信是 Go协程 间事件同步的主要方式。在某个特定的信道上**发送**一个数据,则对应地可以在这个信道上**接收**一个数据,一般情况下是在不同的 Go协程 间发送与接收。 **规则一**:在某个信道上发送数据的事件**发生在**相应的接收事件**之前**。 看下面的代码: ```go 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` 的打印。因为信道的写入事件 `c <- 0` **发生在**读取事件 `<-c` **之前**,而 `<-c` **发生在** `print(a)`**之前**。 **规则二**:信道的关闭事件**发生在**从信道接收到零值(由信道关闭触发)**之前**。 在前面的例子中,可以使用 `close(c)` 来替代 `c <- 0` 语句来保证同样的效果。 **规则三**:对于没有缓存的信道,数据的接收事件**发生在**数据发送完成**之前**。 比如下面的代码(类似上面给出的代码,但是使用了没有缓存的信道,且发送和接收的语句交换了一下): ```go var c = make(chan int) var a string func f() { a = "hello, world" <-c } func main() { go f() c <- 0 print(a) } ``` 上面这段代码依然可以保证可以打印 `hello, world`。因为信道的写入事件 `c <- 0` **发生在**读取事件 `<-c` **之前**,而 `<-c` **发生在**写入事件 `c <- 0` 完成**之前**,同时写入事件 `c <- 0` 的完成**发生在** `print` **之前**。 上面的代码,如果信道是带缓存的(比如 `c = make(chan int, 1)`),程序将不能保证会打印出 `hello, world`,它可能会打印出空字符串,也可能崩溃退出,或者表现出一些其他的症状。 **规则四**:对于容量为 C 的信道,接收第 k 个元素的事件**发生在**第 k+C 个元素的发送**之前**。 规则四是规则三在**带缓存的信道**上的推广。它使得带缓存的信道可以模拟出**计数信号量**:信道中元素的个数**表示**活跃数,信道的容量**表示**最大的可并发数;发送一个元素**意味着**获取一个信号量,接收一个元素**意味着**释放这个信号量。这是一种常见的限制并发的用法。 下面的代码给工作列表中的每个入口都开启一个 Go协程,但是通过配合一个固定长度的信道保证了同时最多有 3 个运行的工作(最多 3 个并发)。 ```go var limit = make(chan int, 3) func main() { for _, w := range work { go func(w func()) { limit <- 1 w() <-limit }(w) } select{} } ``` ### 锁 包 `sync` 实现了两类锁数据类型,分别是 `sync.Mutex` 和 `sync.RWMutex`。 **规则一**:对于类型为 `sync.Mutex` 和 `sync.RWMutex` 的变量 `l`,如果存在 n 和 m 且满足 `n < m`,则 `l.Unlock()` 的第 n 次调用返回发生在`l.Lock()`的第 m 次调用返回**之前**。 比如下面的代码: ```go 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()`的第 1 次调用返回(在函数 `f` 内部)**发生在** `l.Lock()` 的第 2 次调用返回**之前**,后者**发生在** `print` **之前**。 **规则二**:存在类型 `sync.RWMutex` 的变量 `l`,如果 `l.RLock` 的调用返回**发生在** `l.Unlock` 的第 n 次调用返回**之后**,那么其对应的 `l.RUnlock` **发生在** `l.Lock` 的第 n+1 次调用返回**之前**。 ### 一次运行 包 `sync` 还提供了 `Once` 类型用来保证多协程的初始化的安全。多个 Go协程 可以并发执行 `once.Do(f)` 来执行函数 `f`, 且只会有一个 Go协程 会运行 `f()`,其他的 Go 协程会阻塞到那**单次执行的** `f()` 的返回。 **规则一**:函数 `f()` 在 `once.Do(f)` 的单次调用返回**发生在**其他所有的 `once.Do(f)` 调用返回**之前**。 比如下面的代码: ```go 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`。 ### 不正确的同步方式 对某个变量的读操作 `r` 一定概率可以观察到对同一个变量的并发写操作 `w`,但是即使这件事情发生了,也并不意味着**发生在** `r` **之后**的其他读操作可以观察到**发生在** `w` **之前**的其他写操作。 比如下面的代码: ```go var a, b int func f() { a = 1 b = 2 } func g() { print(b) print(a) } func main() { go f() g() } ``` 上面的代码里函数 `g` 可能会先打印 2(b的值),然后打印 0(a的值);可能大家会认为既然 b 的值已经被赋值为 2 了,那么 a 的值肯定被赋值为 1 了,但事实是两个事件的先后在这里是没有办法确定的。 上面的事实可以证明下面的几个常见的错误。 #### 错误一:双重检查锁定 双重检查锁定是一种尝试避免同步开销的尝试。比如下面的例子,`twoprint` 函数可能会被**错误地**编写为: ```go 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”字符串。 #### 错误二:某个值的循环检查 另一个常见的错误用法是对某个值的循环检查,比如下面的代码: ```go var a string var done bool func setup() { a = "hello, world" done = true } func main() { go setup() for !done { } print(a) } ``` 和上一个例子类似,`main`函数中观察到对 `done` 的写操作并不意味着可以观察到对 `a` 的写操作,因此上面的代码依然可能会打印出空字符串。更为恐怖的,由于两个 Go协程 之间缺少同步事件,`main` 函数甚至可能永远无法观察到对 `done` 变量的写操作,导致 `main` 中的 for 循环永远执行下去。 #### 错误三:循环检查的变形 错误二存在一个变形,如下面的代码所示: ```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) } ``` 上面的代码即使 `main` 函数观察到 `g != nil`并且退出了它的 for 循环,依然没有办法保证它可以观察到被初始化的 `g.msg` 值。 避免上面几个错误用法的方式是一样的:显式使用同步语句。