内存模型的目的是为了定义清楚变量的读写在不同执行体里的可见性
在 Golang 中,遵循的内存可见性原则主要包括如下几类:
关于初始化:
1. 如果 package p 引用了 package q,q 的 init() 方法 happens-before p
2. main.main() 方法 happens-after 所有 package 的 init() 方法结束
关于 Goroutine:
3. 执行 goroutine 语句之前的变量赋值 happens-before 该 goroutine 的执行
package main
import (
"log"
"time"
)
var a, b, c int
func main() {
a = 1
b = 2
go func() {
c = a + 2
log.Println(a, b, c) // a、b 先于 go 语句,因此可见
}()
time.Sleep(1 * time.Second)
}
我们可以确定 c=a+2
是happens-after a=1 和 b=2
,所以结果输出是可以确定的 1 2 3
假如把 a=1 和 b=2 放到 go func() 语句后边,结果就是不可预期的了
4. goroutine 内的赋值不保证 happens-before 执行该 goroutine 语句后的语句
package main
import (
"time"
"fmt"
)
var a int
func main() {
go func() {
a = 1
}()
fmt.Println(a) // a 可能是 0,也可能是 1
time.Sleep(1 * time.Second)
}
因为 a=1 没有使用同步机制,并不能保证这个赋值被主 goroutine 可见
关于 Channel:
5. 对一个 Channel 的发送操作 happens-before 相应 Channel 的接收操作
6. 关闭一个 Channel happens-before 从该 Channel 接收到最后的返回值 0
7. 不带缓冲的 Channel 的接收操作 happens-before 相应 Channel 的发送操作
package main
var c = make(chan int, 10)
var a string
func f() {
a = "hello, world"
c <- 0
}
func main() {
go f()
<-c
print(a)
}
上述代码可以确保输出 hello, world,因为 a = "hello, world" happens-before c <- 0,print(a) happens-after <-c, 根据 happens-before 的可传递性,a = "hello, world" happens-before print(a)。把 c<-0 替换成 close(c) 也能保证输出 hello,world,因为关闭操作在 <-c 接收到 0 之前发送。
package main
var c = make(chan int)
var a string
func f() {
a = "hello, world"
<-c
}
func main() {
go f()
c <- 0
print(a)
}
因为 channel 不带缓冲区,也就是说不能存储内容,所以 <-c 没准备好时 c<-0 会阻塞,
因此执行 c<-0 时,f() 函数必然已经执行到 <-c,也就是说 a="hello,world" 必然已经执行完了,因此输出必然是 "hello,world"
关于锁:
8. 任何 sync.Mutex 或 sync.RWMutex 变量 l,定义 n < m, 第 n 次 l.Unlock() happens-before 第 m 次 l.lock() 调用
package main
import "sync"
var l sync.Mutex
var a string
func f() {
a = "hello, world"
l.Unlock()
}
func main() {
l.Lock()
go f()
l.Lock() // 先 unlock 才能二次 lock
print(a)
}
a="hello, world" happens-before l.Unlock() happens-before 第二个 l.Lock() happens-before print(a)
9. once.Do(f) 中的 f() happens-before 任何多个 once.Do(f) 调用的返回,且 f() 有且只有一次调用
import "sync"
var a string
var once sync.Once
func setup() {
a = "hello, world"
}
func doprint() {
once.Do(setup)
print(a)
}
func twoprint() {
go doprint()
go doprint()
}
上面的代码虽然调用两次 doprint(),但实际上 setup 只会执行一次,
并且并发的 once.Do(setup) 都会等待 setup 返回后再继续执行。