内存模型
Go 内存模型
内存模型是非常重要的,理解 Go 的内存模型会就可以明白很多奇怪的竞态条件问题。参考 The Go Memory Model 一文中的描述,Go 内存模型描述的是:在某个 Goroutine 中对变量进行读操作,其同时能够监测到其他 Goroutine 中对该变量进行写操作的条件。
顺序一致性内存模型
var a string
var done bool
func setup() {
a = "hello, world"
done = true
}
func main() {
go setup()
for !done {}
print(a)
}
我们创建了 setup 线程,用于对字符串 a 的初始化工作,初始化完成之后设置 done 标志为 true。main 函数所在的主线程中,通过 for !done {}检测 done 变为 true 时,认为字符串初始化工作完成,然后进行字符串的打印工作。但是 Go 语言并不保证在 main 函数中观测到的对 done 的写入操作发生在对字符串 a 的写入的操作之后,因此程序很可能打印一个空字符串。更糟糕的是,因为两个线程之间没有同步事件,setup 线程对 done 的写入操作甚至无法被 main 线程看到,main 函数有可能陷入死循环中。
在 Go 语言中,同一个 Goroutine 线程内部,顺序一致性内存模型是得到保证的。但是不同的 Goroutine 之间,并不满足顺序一致性内存模型,需要通过明确定义的同步事件来作为同步的参考。如果两个事件不可排序,那么就说这两个事件是并发的。为了最大化并行,Go 语言的编译器和处理器在不影响上述规定的前提下可能会对执行语句重新排序(CPU 也会对一些指令进行乱序执行)。
因此,如果在一个 Goroutine 中顺序执行 a = 1; b = 2;两个语句,虽然在当前的 Goroutine 中可以认为 a = 1;语句先于 b = 2;语句执行,但是在另一个 Goroutine 中 b = 2;语句可能会先于 a = 1;语句执行,甚至在另一个 Goroutine 中无法看到它们的变化(可能始终在寄存器中)。也就是说在另一个 Goroutine 看来, a = 1; b = 2;两个语句的执行顺序是不确定的。如果一个并发程序无法确定事件的顺序关系,那么程序的运行结果往往会有不确定的结果。比如下面这个程序:
func main() {
go println("你好, 世界")
}
根据 Go 语言规范,main 函数退出时程序结束,不会等待任何后台线程。因为 Goroutine 的执行和 main 函数的返回事件是并发的,谁都有可能先发生,所以什么时候打印,能否打印都是未知的。用前面的原子操作并不能解决问题,因为我们无法确定两个原子操作之间的顺序。
链接
- https://mp.weixin.qq.com/s/vvKNAarcc3kz1hz9o4B1ZQ Golang 并发编程核心篇 —— 内存可见性