Golang内存模型
参考自:https://golang.org/ref/mem
Golang的内存模型描述了这样的一种场景:在一个goroutine中对一个变量的读取能保证是由不同gorountine写入相同变量所产生的。
Happens Before
在单个goroutine中,只有在满足不改变语言规范所定义的行为时,编译器才能对单个goroutine所执行的读写进行重新排序。但由于重新排序,一个goroutine所观察到的执行顺序可能与另一个goroutine察觉到的执行顺序不同。
为了指定读写要求,在go程序中定义了一个叫Happens Before的偏序关系——如果事件e1发生在事件e2之前,那么我们说e2发生在e1之后。同样,如果e1不在e2之前发生并且在e2之后也没有发生,那么我们说e1和e2同时发生。
在单个goroutine中,Happens Before的顺序就是程序所表现出来的顺序。
为了保证对变量的读取R可以读取到由特定的对变量的写入W,即W是R可以观察到的唯一写入,必须要满足以下两个条件:
- W发生到R之前;
- 任何对变量的其他写入要么发生在w之前,要么发生在r之后;
变量的初始化为零值,其实也是内存模型中的零值写入。
Synchronization
初始化
程序的初始化是在单个goroutine中进行的,但goroutine可以创建其他goroutine,这是并发的。
如果一个package引入了另一个package,即被引入的package会先初始化。
main.main的开始必须要在所有init函数完成之后。
Goroutine的创建
以下面的为例子,f()打印出hello world可能会在hello()结束后才打印。
1 | var a string |
Goroutine的销毁
无法保证goroutine的退出在程序中的任何其他事件发生之前发生。
1 | var a string |
对a的赋值很可能在下面的print中看不到,因为缺乏同步。
Channel的同步
通道通信是goroutine之间同步的主要方法。channel的发送必定发生在该channel接受完成之前。
1 | var c = make(chan int, 10) |
这样就能保证打印出hello, world。
另外,channel的关闭会发生在返回零值的接收之前,这样用close(c)替代c<-0也可以产生相同的保证行为。
而对于缺乏buffer的channel,其接收会发生在该channel的发送完成之前,例如这样也可以保证打印出正确的hello world,这里的发送和接收顺序与上面的例子相反。
1 | var c = make(chan int) |
但如果channel是带有buffer,就无法保证打印出hello world了。
在容量为C的通道上的第k个接收发生在该通道的第k + C个发送完成之前,因为不从channel接收数据就无法继续写入。
该规则将前一个规则推广到缓冲通道。 它允许通过缓冲的通道对计数信号量进行建模:channel中的项目数量对应于活动使用的数量,channel的容量对应于同时使用的最大数量,发送一个项目获取信号量,接收项目则会释放信号量。通过这种操作就可以限制其并发。
1 | var limit = make(chan int, 3) |
Locks
sync包里实现了两种锁相关的数据类型:sync.Mutex和sync.RWMutex。通过锁的使用,我们可以在goroutine中保证同步。这样的一个程序就可以顺利打印出hello world。
1 | var l sync.Mutex |
Once
sync包还提供了一种初始化的安全机制,通过使用Once数据类型,多个线程都可以执行once.Do(f),但只有一个会运行f(),其他的调用将会block直接f()返回。
1 | var a string |
在这种机制下,a的赋值将会在打印之前执行。
Incorrect synchronization
需要注意的是读取R可能会观察到与R同时发生的写入W所写入的值,但这并不意味着在R之后的读取会观察到在W之前所发生的写入。
1 | var a, b int |
这里可能发生的情况是g()打印出了2和0,也就是即便g()已经读取了f()里面对b的写入,但这不意味着g()里面的a能够读取到f()中在b写入之前的a。
同理,类似的错误也会发生在同步的使用:
1 | var a string |
这里并不意味着能够观察到done设置为true,就隐式地说明a已经被初始化。
另一种典型错误则是忙等,这种情况下并不意味着done被设置为true后,能够表示a已经被初始化,可以跳出for循环。真实情况是,此时print(a),a可能还是空字符串。
1 | var a string |
应对这些问题也很简单,使用显式地同步。