简介
Go 内存模型指定了在并发多 goroutine 读相同变量的时候,变量的可见性条件。也就是 goroutine 在读取一个变量的值的时候,能够看到其它 goroutine 对这个变量进行的写的结果。
指令重排和可见性问题
可见性问题
看一下这个例子,g1 goruntine 就是将a、b变量进行赋值,在 g2 goruntine 中打印 b 的值。需要注意的是,即使这里打印出 b 的值是 2,但是依然可能在打印 a 的值时,打印出初始值 0,而不是 1。这是因为,程序运行的时候,不能保证 g2 看到的 a 和 b 的赋值有先后关系。
看到这里,你可能会有疑问,我都运行这个程序几百万次了,怎么也没有观察到这种现象?能不能观察到和提供保证是两码事儿,由于 CPU 架构和 Go 编译器的不同,即使你运行程序时没有遇到这些现象,也不代表 Go 可以 100% 保证不会出现这些问题。
package main
var a, b int
func f() {
a = 1
b = 2
}
func g() {
print(b)
print(a)
}
func main() {
go f() // g1
g() // g2
}
指令重排
X = 0
for i in range(100):
X = 1
print X
假设我们写了上面这样的一段代码,在循环里面都只会打印出 100 次 1,那么编译器可能会将指令进行重新编排,优化成了下面这种方式
X = 1
for i in range(100):
print X
这么优化完之后,单个 goroutine 中并不会改变程序的执行,这时候同样会输出 100 次 1 ,并且减少了 100 次赋值操作。但是,如果与此同时我们存在一个另外一个 goroutine 干了另外一个事情 X = 0, 那么这个输出就变的不可预知了,就有可能是 1001111101…,所以如果存在并发读写的时候,都需要进行串行化访问。
Happen Before
含义
Go 内存模型通过 happens-before 定义两个事件(读、写 action)的顺序:如果事件 e1 happens before 事件 e2,那么,我们就可以说事件 e2 在事件 e1 之后发生(happens after)。如果 e1 不是 happens before e2, 同时也不 happens after e2,那么,我们就可以说事件 e1 和 e2 是同时发生的。
在单一独立的 goruntine 中先行发生的顺序就是程序中表达的顺序
假设有个变量 v ,如果要保证对 变量 v 的读操作 r 可以观察到一个对 变量 v 的写操作 w,需要满足 2 个条件:
r不能先行发生于w- 在
w后r前没有对v的其他的写操作
如果要保证 变量 v 的读操作 r 一定能观察到到一个对 变量 v 的写操作 w,需确保 w 是 r 允许看到的唯一写操作,在满足 2 个更为严格的条件:
w发生在r之前- 其他对共享变量
v的写操作要么发生在w之前或r之后
这个条件的要求比第一个条件更强,它需要确保没有其它写入操作与 w 或 r 并发。在单个 goroutine 当中这两个条件是等价的,因为单个 goroutine 中不存在并发,在多个 goroutine 中就必须使用同步语义来确保顺序,这样才能到保证能够监测到预期的写入。
重点
- 在 Go 语言中,对变量进行零值的初始化就是一个写操作。
- 如果对超过机器 word(64bit、32bit 或者其它)大小的值进行读写,那么,就可以看作是对拆成 word 大小的几个读写无序进行。
Go 保证的 happens-before 关系
init 函数
- 如果一个包
p导入包q,则q的init函数的完成发生在任何p的init开始之前 main函数的启动发生在所有init函数完成之后
goruntine创建
启动新 goroutine的 go 语句发生在 goroutine 开始执行之前,这里一定会输出 "hello,world" 的
var a string
func f() {
print(a)
}
func hello() {
a = "hello, world"
go f()
}
goruntine销毁
goroutine 退出的时候,是没有任何 happens-before 保证的。所以,如果你想观察某个 goroutine 的执行效果,你需要使用同步机制建立 happens-before 关系,比如 Mutex 或者 Channel
Channel
- Channel 上的 send 发生在该通道的相应 send 的那次 Recv 完成之前
var c = make(chan int, 10)
var a string
func f() {
a = "hello, world"
c <- 0
}
func main() {
go f()
<-c
print(a)
}
-
close一个Channel的调用,肯定发生在从关闭的 Channel 中读取出一个零值之前 -
对于
unbuffered的Channel,也就是容量是 0 的Channel,从此Channel中读取数据的调用一定发生在往此Channel发送数据的调用完成之前
var c = make(chan int)
var a string
func f() {
a = "hello, world"
<-c
}
func main() {
go f()
c <- 0
print(a)
}
不正确的同步方式
这是一种典型的不正确的同步方式,一个 goruntine 读取另外一个 goruntine 设置的值,来做逻辑判断,不能保证观察到done 意味着观察到写入a
var a string
var done bool
func setup() {
a = "hello, world"
done = true
}
func main() {
go setup()
for !done {
}
print(a)
}
参考
go.dev/ref/mem 建议仔细阅读