<audio id="audio" title="15 | 内存模型：Go如何保证并发读写的顺序？" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/8b/aa/8b4b79d1a716766582d453c45eb87daa.mp3"></audio>

你好，我是鸟窝。

Go官方文档里专门介绍了Go的[内存模型](https://golang.org/ref/mem)，你不要误解这里的内存模型的含义，它并不是指Go对象的内存分配、内存回收和内存整理的规范，它描述的是并发环境中多goroutine读相同变量的时候，变量的可见性条件。具体点说，就是指，在什么条件下，goroutine在读取一个变量的值的时候，能够看到其它goroutine对这个变量进行的写的结果。

由于CPU指令重排和多级Cache的存在，保证多核访问同一个变量这件事儿变得非常复杂。毕竟，不同CPU架构（x86/amd64、ARM、Power等）的处理方式也不一样，再加上编译器的优化也可能对指令进行重排，所以编程语言需要一个规范，来明确多线程同时访问同一个变量的可见性和顺序（ Russ Cox在麻省理工学院 [6.824 分布式系统Distributed Systems课程](https://pdos.csail.mit.edu/6.824/) 的一课，专门介绍了相关的[知识](http://nil.csail.mit.edu/6.824/2016/notes/gomem.pdf)）。在编程语言中，这个规范被叫做内存模型。

除了Go，Java、C++、C、C#、Rust等编程语言也有内存模型。为什么这些编程语言都要定义内存模型呢？在我看来，主要是两个目的。

- 向广大的程序员提供一种保证，以便他们在做设计和开发程序时，面对同一个数据同时被多个goroutine访问的情况，可以做一些串行化访问的控制，比如使用Channel或者sync包和sync/atomic包中的并发原语。
- 允许编译器和硬件对程序做一些优化。这一点其实主要是为编译器开发者提供的保证，这样可以方便他们对Go的编译器做优化。

既然内存模型这么重要，今天，我们就来花一节课的时间学习一下。

首先，我们要先弄明白重排和可见性的问题，因为它们影响着程序实际执行的顺序关系。

# 重排和可见性的问题

**由于指令重排，代码并不一定会按照你写的顺序执行**。

举个例子，当两个goroutine同时对一个数据进行读写时，假设goroutine g1对这个变量进行写操作w，goroutine g2同时对这个变量进行读操作r，那么，如果g2在执行读操作r的时候，已经看到了g1写操作w的结果，那么，也不意味着g2能看到在w之前的其它的写操作。这是一个反直观的结果，不过的确可能会存在。

接下来，我再举几个具体的例子，带你来感受一下，重排以及多核CPU并发执行导致程序的运行和代码的书写顺序不一样的情况。

先看第一个例子，代码如下：

```
var a, b int

func f() {
	a = 1 // w之前的写操作
	b = 2 // 写操作w
}

func g() {
	print(b) // 读操作r
	print(a) // ???
}

func main() {
	go f() //g1
	g() //g2
}

```

可以看到，第9行是要打印b的值。需要注意的是，即使这里打印出的值是2，但是依然可能在打印a的值时，打印出初始值0，而不是1。这是因为，程序运行的时候，不能保证g2看到的a和b的赋值有先后关系。

再来看一个类似的例子。

```
var a string
var done bool

func setup() {
	a = &quot;hello, world&quot;
	done = true
}

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

```

在这段代码中，主goroutine main即使观察到done变成true了，最后读取到的a的值仍然可能为空。

更糟糕的情况是，main根本就观察不到另一个goroutine对done的写操作，这就会导致main程序一直被hang住。甚至可能还会出现**半初始化**的情况，比如：

```
type T struct {
	msg string
}

var g *T

func setup() {
	t := new(T)
	t.msg = &quot;hello, world&quot;
	g = t
}

func main() {
	go setup()
	for g == nil {
	}
	print(g.msg)
}

```

即使main goroutine观察到g不为nil，也可能打印出空的msg（第17行）。

看到这里，你可能要说了，我都运行这个程序几百万次了，怎么也没有观察到这种现象？我可以这么告诉你，能不能观察到和提供保证（guarantee）是两码事儿。由于CPU架构和Go编译器的不同，即使你运行程序时没有遇到这些现象，也不代表Go可以100%保证不会出现这些问题。

刚刚说了，程序在运行的时候，两个操作的顺序可能不会得到保证，那该怎么办呢？接下来，我要带你了解一下Go内存模型中很重要的一个概念：happens-before，这是用来描述两个时间的顺序关系的。如果某些操作能提供happens-before关系，那么，我们就可以100%保证它们之间的顺序。

# happens-before

在一个goroutine内部，程序的执行顺序和它们的代码指定的顺序是一样的，即使编译器或者CPU重排了读写顺序，从行为上来看，也和代码指定的顺序一样。

这是一个非常重要的保证，我们一定要记住。

我们来看一个例子。在下面的代码中，即使编译器或者CPU对a、b、c的初始化进行了重排，但是打印结果依然能保证是1、2、3，而不会出现1、0、0或1、0、1等情况。

```
func foo() {
    var a = 1
    var b = 2
    var c = 3

    println(a)
    println(b)
    println(c)
}

```

但是，对于另一个goroutine来说，重排却会产生非常大的影响。**因为Go只保证goroutine内部重排对读写的顺序没有影响**，比如刚刚我们在讲“可见性”问题时提到的三个例子，那该怎么办呢？这就要用到happens-before关系了。

如果两个action（read 或者 write）有明确的happens-before关系，你就可以确定它们之间的执行顺序（或者是行为表现上的顺序）。

Go内存模型通过happens-before定义两个事件（读、写action）的顺序：如果事件e1  happens before 事件e2，那么，我们就可以说事件e2在事件e1之后发生（happens after）。如果e1 不是happens before e2， 同时也不happens after e2，那么，我们就可以说事件e1和e2是同时发生的。

如果要保证对“变量**v**的读操作**r**”能够观察到一个对“变量**v**的写操作**w**”，并且**r**只能观察到**w**对变量**v**的写，没有其它对v的写操作，也就是说，我们要保证**r**绝对能观察到**w**操作的结果，那么就需要同时满足两个条件：

1. w happens before r；
1. 其它对v的写操作（w2、w3、w4, ......） 要么happens before w，要么happens after r，绝对不会和w、r同时发生，或者是在它们之间发生。

你可能会说，这是很显然的事情啊，但我要和你说的是，这是一个非常严格、严谨的数学定义。

对于单个的goroutine来说，它有一个特殊的happens-before关系，Go内存模型中是这么讲的：

> 
Within a single goroutine, the happens-before order is the order expressed by the program.


我来解释下这句话。它的意思是，在单个的goroutine内部， happens-before的关系和代码编写的顺序是一致的。

其实，在这一章的开头我已经用橙色把这句话标注出来了。我再具体解释下。

在goroutine内部对一个局部变量v的读，一定能观察到最近一次对这个局部变量v的写。如果要保证多个goroutine之间对一个共享变量的读写顺序，在Go语言中，可以使用并发原语为读写操作建立happens-before关系，这样就可以保证顺序了。

说到这儿，我想先给你补充三个Go语言中和内存模型有关的小知识，掌握了这些，你就能更好地理解下面的内容。

1. 在Go语言中，对变量进行零值的初始化就是一个写操作。
1. 如果对超过机器word（64bit、32bit或者其它）大小的值进行读写，那么，就可以看作是对拆成word大小的几个读写无序进行。
1. Go并不提供直接的CPU屏障（CPU fence）来提示编译器或者CPU保证顺序性，而是使用不同架构的内存屏障指令来实现统一的并发原语。

接下来，我就带你学习下Go语言中提供的happens-before关系保证。

# Go语言中保证的happens-before关系

除了单个goroutine内部提供的happens-before保证，Go语言中还提供了一些其它的happens-before关系的保证，下面我来一个一个介绍下。

## init函数

应用程序的初始化是在单一的goroutine执行的。如果包p导入了包q，那么，q的init函数的执行一定 happens before  p的任何初始化代码。

这里有一个特殊情况需要你记住：**main函数一定在导入的包的init函数之后执行**。

包级别的变量在同一个文件中是按照声明顺序逐个初始化的，除非初始化它的时候依赖其它的变量。同一个包下的多个文件，会按照文件名的排列顺序进行初始化。这个顺序被定义在[Go语言规范](https://golang.org/ref/spec#Program_initialization_and_execution)中，而不是Go的内存模型规范中。你可以看看下面的例子中各个变量的值：

```
var (
	a = c + b  // == 9
	b = f()    // == 4
	c = f()    // == 5
	d = 3      // == 5 全部初始化完成后
)

func f() int {
	d++
	return d
}

```

具体怎么对这些变量进行初始化呢？Go采用的是依赖分析技术。不过，依赖分析技术保证的顺序只是针对同一包下的变量，而且，只有引用关系是本包变量、函数和非接口的方法，才能保证它们的顺序性。

同一个包下可以有多个init函数，甚至一个文件中也可以包含多个相同签名的init函数。

刚刚讲的这些都是不同包的init函数执行顺序，下面我举一个具体的例子，把这些内容串起来，你一看就明白了。

这个例子是一个**main**程序，它依赖包p1，包p1依赖包p2，包p2依赖p3。

<img src="https://static001.geekbang.org/resource/image/d5/2a/d5059fab1977602934339e18f9eddb2a.jpg" alt="">

为了追踪初始化过程，并输出有意义的日志，我定义了一个辅助方法，打印出日志并返回一个用来初始化的整数值：

```
func Trace(t string, v int) int {
    fmt.Println(t, &quot;:&quot;, v)
    return v
}

```

包**p3**包含两个文件，分别定义了一个init函数。第一个文件中定义了两个变量，这两个变量的值还会在init函数中进行修改。

我们来分别看下包p3的这两个文件：

```
// lib1.go in p3

var V1_p3 = trace.Trace(&quot;init v1_p3&quot;, 3)
var V2_p3 = trace.Trace(&quot;init v2_p3&quot;, 3)


func init() {
    fmt.Println(&quot;init func in p3&quot;)
    V1_p3 = 300
    V2_p3 = 300
}

```

```
// lib2.go in p3

func init() {
    fmt.Println(&quot;another init func in p3&quot;)
}

```

下面再来看看包p2。包p2定义了变量和init函数。第一个变量初始化为2，并在init函数中更改为200。第二个变量是复制的p3.V2_p3。

```
var V1_p2 = trace.Trace(&quot;init v1_p2&quot;, 2)
var V2_p2 = trace.Trace(&quot;init v2_p2&quot;, p3.V2_p3)

func init() {
    fmt.Println(&quot;init func in p2&quot;)
    V1_p2 = 200
}

```

包**p1**定义了变量和init函数。它的两个变量的值是复制的p2对应的两个变量值。

```
var V1_p1 = trace.Trace(&quot;init v1_p1&quot;, p2.V1_p2)
var V2_p1 = trace.Trace(&quot;init v2_p1&quot;, p2.V2_p2)

func init() {
    fmt.Println(&quot;init func in p1&quot;)
}

```

**main**定义了init函数和main函数。

```
func init() {
    fmt.Println(&quot;init func in main&quot;)
}


func main() {
    fmt.Println(&quot;V1_p1:&quot;, p1.V1_p1)
    fmt.Println(&quot;V2_p1:&quot;, p1.V2_p1)
}

```

运行main函数会依次输出p3、p2、p1、main的初始化变量时的日志（变量初始化时的日志和init函数调用时的日志）：

```
// 包p3的变量初始化
init v1_p3 : 3
init v2_p3 : 3
// p3的init函数
init func in p3
// p3的另一个init函数 
another init func in p3

// 包p2的变量初始化
init v1_p2 : 2
init v2_p2 : 300
// 包p2的init函数
init func in p2

// 包p1的变量初始化
init v1_p1 : 200
init v2_p1 : 300
// 包p1的init函数
init func in p1

// 包main的init函数
init func in main
// main函数
V1_p1: 200
V2_p1: 300

```

下面，我们再来看看goroutine对happens-before关系的保证情况。

## goroutine

首先，我们需要明确一个规则：**启动goroutine的go语句的执行，一定happens before此goroutine内的代码执行。**

根据这个规则，我们就可以知道，如果go语句传入的参数是一个函数执行的结果，那么，这个函数一定先于goroutine内部的代码被执行。

我们来看一个例子。在下面的代码中，第8行a的赋值和第9行的go语句是在同一个goroutine中执行的，所以，在主goroutine看来，第8行肯定happens before 第9行，又由于刚才的保证，第9行子goroutine的启动happens before 第4行的变量输出，那么，我们就可以推断出，第8行happens before 第4行。也就是说，在第4行打印a的值的时候，肯定会打印出“hello world”。

```
var a string

func f() {
	print(a)
}

func hello() {
	a = &quot;hello, world&quot;
	go f()
}

```

刚刚说的是启动goroutine的情况，goroutine退出的时候，是没有任何happens-before保证的。所以，如果你想观察某个goroutine的执行效果，你需要使用同步机制建立happens-before关系，比如Mutex或者Channel。接下来，我会讲Channel的happens-before的关系保证。

## Channel

Channel是goroutine同步交流的主要方法。往一个Channel中发送一条数据，通常对应着另一个goroutine从这个Channel中接收一条数据。

通用的Channel happens-before关系保证有4条规则，我分别来介绍下。

**第1条规则是**，往Channel中的发送操作，happens before 从该Channel接收相应数据的动作完成之前，即第n个send一定happens before第n个receive的完成。

```
var ch = make(chan struct{}, 10) // buffered或者unbuffered
var s string

func f() {
	s = &quot;hello, world&quot;
	ch &lt;- struct{}{}
}

func main() {
	go f()
	&lt;-ch
	print(s)
}

```

在这个例子中，s的初始化（第5行）happens before 往ch中发送数据， 往ch发送数据 happens before从ch中读取出一条数据（第11行），第12行打印s的值 happens after第11行，所以，打印的结果肯定是初始化后的s的值“hello world”。

**第2条规则是**，close一个Channel的调用，肯定happens before 从关闭的Channel中读取出一个零值。

还是拿刚刚的这个例子来说，如果你把第6行替换成 close(ch)，也能保证同样的执行顺序。因为第11行从关闭的ch中读取出零值后，第6行肯定被调用了。

**第3条规则是**，对于unbuffered的Channel，也就是容量是0的Channel，从此Channel中读取数据的调用一定happens before 往此Channel发送数据的调用完成。

所以，在上面的这个例子中呢，如果想保持同样的执行顺序，也可以写成这样：

```
var ch = make(chan int)
var s string

func f() {
	s = &quot;hello, world&quot;
	&lt;-ch
}

func main() {
	go f()
	ch &lt;- struct{}{}
	print(s)
}

```

如果第11行发送语句执行成功（完毕），那么根据这个规则，第6行（接收）的调用肯定发生了（执行完成不完成不重要，重要的是这一句“肯定执行了”），那么s也肯定初始化了，所以一定会打印出“hello world”。

这一条比较晦涩，但是，因为Channel是unbuffered的Channel，所以这个规则也成立。

**第4条规则是**，如果Channel的容量是m（m&gt;0），那么，第n个receive一定happens before 第 n+m 个 send的完成。

前一条规则是针对unbuffered channel的，这里给出了更广泛的针对buffered channel的保证。利用这个规则，我们可以实现信号量（Semaphore）的并发原语。Channel的容量相当于可用的资源，发送一条数据相当于请求信号量，接收一条数据相当于释放信号。关于信号量这个并发原语，我会在下一讲专门给你介绍一下，这里你只需要知道它可以控制多个资源的并发访问，就可以了。

## Mutex/RWMutex

对于互斥锁Mutex m或者读写锁RWMutex m，有3条happens-before关系的保证。

1. 第n次的m.Unlock一定happens before第n+1 m.Lock方法的返回；
1. 对于读写锁RWMutex m，如果它的第n个m.Lock方法的调用已返回，那么它的第n个m.Unlock的方法调用一定happens before 任何一个m.RLock方法调用的返回，只要这些m.RLock方法调用 happens after 第n次m.Lock的调用的返回。这就可以保证，只有释放了持有的写锁，那些等待的读请求才能请求到读锁。
1. 对于读写锁RWMutex m，如果它的第n个m.RLock方法的调用已返回，那么它的第k （k&lt;=n）个成功的m.RUnlock方法的返回一定happens before 任意的m.RUnlockLock方法调用，只要这些m.Lock方法调用happens after第n次m.RLock。

读写锁的保证有点绕，我再带你看看官方的描述：

> 
对于读写锁l的 l.RLock方法调用，如果存在一个**n**，这次的l.RLock调用 happens after 第n次的l.Unlock，那么，和这个RLock相对应的l.RUnlock一定happens before 第n+1次l.Lock。意思是，读写锁的Lock必须等待既有的读锁释放后才能获取到。


我再举个例子。在下面的代码中，第6行第一次的Unlock一定happens before第二次的Lock（第12行），所以这也能保证正确地打印出“hello world”。

```
var mu sync.Mutex
var s string

func foo() {
	s = &quot;hello, world&quot;
	mu.Unlock()
}

func main() {
	mu.Lock()
	go foo()
	mu.Lock()
	print(s)

```

## WaitGroup

接下来是WaitGroup的保证。

对于一个WaitGroup实例wg，在某个时刻t0时，它的计数值已经不是零了，假如t0时刻之后调用了一系列的wg.Add(n)或者wg.Done()，并且只有最后一次调用wg的计数值变为了0，那么，可以保证这些wg.Add或者wg.Done()一定 happens before t0时刻之后调用的wg.Wait方法的返回。

这个保证的通俗说法，就是**Wait方法等到计数值归零之后才返回**。

## Once

我们在[第8讲](https://time.geekbang.org/column/article/301113)学过Once了，相信你已经很熟悉它的功能了。它提供的保证是：**对于once.Do(f)调用，f函数的那个单次调用一定happens before 任何once.Do(f)调用的返回**。换句话说，就是函数f一定会在Do方法返回之前执行。

还是以hello world的例子为例，这次我们使用Once并发原语实现，可以看下下面的代码：

```
var s string
var once sync.Once

func foo() {
	s = &quot;hello, world&quot;
}

func twoprint() {
	once.Do(foo)
	print(s)
}

```

第5行的执行一定happens before第9行的返回，所以执行到第10行的时候，sd已经初始化了，所以会正确地打印“hello world”。

最后，我再来说说atomic的保证。

## atomic

其实，Go内存模型的官方文档并没有明确给出atomic的保证，有一个相关的issue [go# 5045](https://github.com/golang/go/issues/5045)记录了相关的讨论。光看issue号，就知道这个讨论由来已久了。Russ Cox想让atomic有一个弱保证，这样可以为以后留下充足的可扩展空间，所以，Go内存模型规范上并没有严格的定义。

对于Go 1.15的官方实现来说，可以保证使用atomic的Load/Store的变量之间的顺序性。

在下面的例子中，打印出的a的结果总是1，但是官方并没有做任何文档上的说明和保证。

依照Ian Lance Taylor的说法，Go核心开发组的成员几乎没有关注这个方向上的研究，因为这个问题太复杂，有很多问题需要去研究，所以，现阶段还是不要使用atomic来保证顺序性。

```
func main() {
	var a, b int32 = 0, 0

	go func() {
		atomic.StoreInt32(&amp;a, 1)
		atomic.StoreInt32(&amp;b, 1)
	}()

	for atomic.LoadInt32(&amp;b) == 0{
		runtime.Gosched()
	}
    fmt.Println(atomic.LoadInt32(&amp;a))
}

```

# 总结

Go的内存模型规范中，一开始有这么一段话：

> 
If you must read the rest of this document to understand the behavior of your program, you are being too clever.


> 
Don't be clever.


我来说说我对这句话的理解：你通过学习这节课来理解你的程序的行为是聪明的，但是，不要自作聪明。

谨慎地使用这些保证，能够让你的程序按照设想的happens-before关系执行，但是不要以为完全理解这些概念和保证，就可以随意地制造所谓的各种技巧，否则就很容易掉进“坑”里，而且会给代码埋下了很多的“定时炸弹”。

比如，Go里面已经有值得信赖的互斥锁了，如果没有额外的需求，就不要使用Channel创造出自己的互斥锁。

当然，我也不希望你畏手畏脚地把思想局限住，我还是建议你去做一些有意义的尝试，比如使用Channel实现信号量等扩展并发原语。

<img src="https://static001.geekbang.org/resource/image/dc/4d/dc68fc5f93a4af96c8f4d45d6282104d.jpg" alt="">

# 思考题

我们知道，Channel可以实现互斥锁，那么，我想请你思考一下，它是如何利用happens-before关系保证锁的请求和释放的呢？

欢迎在留言区写下你的思考和答案，我们一起交流讨论。如果你觉得有所收获，也欢迎你把今天的内容分享给你的朋友或同事。
