官方介绍在 The Go Memory Model
首先来看段代码:
var a string
var done bool
func setup() {
a = "hello, world"
done = true
}
func main() {
go setup()
for !done {
}
print(a)
}
看起来,a一定会输出为"hello world",但实际上这个程序也可能打印一个空字符串,因为不能保证在 main 中,观察对 done 的写入就意味着能观察到对 a 的写入:
- 编译器可能把无关的代码乱序以提高效率
- CPU 可能会产生执行上的乱序(比如 Intel 的 Store-Load 乱序)
下面是go的内存模型中带有同步语义的保证:
初始化
创建 goruntine
启动新 goroutine 的 go 语句在 goroutine 开始之前执行
var a string
func f() {
print(a)
}
func hello() {
a = "hello, world"
go f()
}
可以确定的是 a = "hello world"
销毁 goruntine
var a string
func hello() {
go func() { a = "hello" }()
print(a)
}
channel 通信
对于有缓冲的channel,send(生产者)先于 receive(消费者)完成
var c = make(chan int, 10)
var a string
func f() {
a = "hello, world"
c <- 0 // send(生产者)
}
func main() {
go f()
<-c // receive(消费者)
print(a)
}
可以确定稳定打印出"hello, world",执行顺序:a = "hello, world" -> send -> receive -> print
对于无缓冲的channel,receive(消费者)先于 send(生产者)完成
var c = make(chan int)
var a string
func f() {
a = "hello, world"
<-c // receive(消费者)
}
func main() {
go f()
c <- 0 // send(生产者)
print(a)
}
对比可以发现,channel变成非缓冲,同时send和receive的位置发生了转换,仍然可以确定稳定打印出"hello, world",执行顺序:a = "hello, world" -> receive -> send -> print
Lock
var l sync.Mutex
var a string
func f() {
a = "hello, world"
l.Unlock()
}
func main() {
l.Lock()
go f()
l.Lock()
print(a)
}
Once
TODO
不正确的同步
var a, b int
func f() {
a = 1
b = 2
}
func g() {
print(b)
print(a)
}
func main() {
go f()
g()
}
这个和最开头的例子差不多,看起来我们会认为会有2个前提:a = 1
一定会先于b = 2
执行,print(b)
一定会先于print(a)
执行。
所以排列组合如下:
1 | 2 | 3 | 4 | 结果 |
---|---|---|---|---|
a=1 | print(a) | b=2 | print(b) | 1 2 |
a=1 | b=2 | print(a) | print(b) | 1 2 |
print(b) | print(a) | a=1 | b=2 | 0 0 |
print(b) | a=1 | print(a) | b=2 | 0 1 |
但实际上,我们可能得出的结果是:2 0,也就是说上面2个前提是不存在的,所以老老实实的用其他同步机制来保证正确的结果。
不正确的编译
TODO
go 官方的建议
所以,综上,最终 go 官方的建议:
通过多个goruntine同时访问来修改数据的行为,必须将这些访问序列化,为了访问序列化来保护数据,可以通过channel操作和其他同步原语(sync&sync/atomic)来实现。
网友评论