go 内存模型
介绍
go 内存模型是在特定的情况下可以保证一个goroutine
观察到的数据和另一个不同的goroutine
写入的数据一致。
happens before
在一个goroutine
中,写和读必须按照程序定义的顺序去执行。也就是说,只有在指令重排序不会改变语言规范定义的goroutine
中的行为时,编译器和处理器才可以重新排序在单个goroutine
中执行的读和写操作。 由于指令重排序,一个goroutine
观察到的是顺序可能和另外一个goroutine
观察到的顺序不同。例如 a=1
;b=2
;其他goroutine
可能观察到b
在a
之前更新数据。
为了明确读和写的顺序,引入了happens before
概念,来表示Go程序中内存操作执行的部分顺序。如果e1 happens before
e2 ,我们也可以说e2 happens after
e1; 如果e1 不happens before
e2 也不happens after
e2,那么我们可以说 e1和e2 是并发的。
在一个goroutine
中happens before
的顺序就是程序的顺序
一个读操作r
读取数据v
如果能观察到v的写操作w
就得满足以下条件
- r 不
happens before
w - 没有其他写操作在w 之后和r 之前
为了保证变量v的读操作r
读取到v
的写操作w
,若w
是唯一的写操作,假如符合以下两个条件 r
能够观察到w
的操作后的数据
- w
happens before
r - 对共享变量v的任何其他写入要么
happens-before
w,要么happens-after
r。
这两个条件比之前的更强,它要求没有其他写操作于w或者r 同时发生。
在一个goroutine
中是没有并发的,r
能够观察到最近w
写入v
的值。当多个goroutine
同时访问共享变量v
时,他们必须
使用同步事件去建立happens-before
条件 确保读操作能够观察到预期的写操作。
变量v
的类型值为0的初始化行为就像在内存模型中写入一样。
对大于单个机器字的值的读写行为与多个机器字大小的操作一样,顺序未指定。
同步
初始化
程序初始化在一个goroutine
中,但是这个goroutine
可能创建其他的goroutine
它们是并发执行的。
如果包p
导入了包q
, q
的init
函数happens-before
任何p
的函数。main
方法happens-after
所有init
方法.
goroutine 创建
go
关键字声明启动一个goroutine
happens-before
该goroutine
启动。
例如以下程序:
var a string
func f() {
print(a)
}
func hello() {
a = "hello, world"
go f()
}
调用hello
方法将会打印出hello world
.
goroutine 销毁
goroutine
的退出无法保证 happens-before
任何事件,例如
var a string
func hello() {
go func() { a = "hello" }()
print(a)
}
对a
的赋值没有任何的同步操作,因此无法保证它能被其他的goroutine
观察到,实际上偏激的编译器可能会删除该语句。
如果一个goroutine
的影响必须被另一个goroutine
观察到,可以使用一个同步机制,比如锁或者通道通信来建立一个相对的顺序。
channel 通讯
在两个goroutine 之间通信主要是通过channel(管道),通常有一个goroutine 在往一个特定的channel 发送消息,对应的还会有一个不同的goroutine在这个channel 接收消息。
一个发送消息到channel happens-before
从对应的channel接收消息完成
var c = make(chan int, 10)
var a string
func f() {
a = "hello, world" //1 1 happens before 2
c <- 0 //2
}
func main() {
go f()
<-c //3 // 3 happens before 4
print(a) //4
}
以上程序能保证main 函数打印出hello world
。因为看上面序号 1 happens before
2 3 happens before
4 然后2 happens before
3 , 所以a 的赋值一定在打印之前。
关闭一个channel happens before 从该channel 中接收一个值(仅无缓冲channel
如下面的例子 调换了 c<- 0
和<- c
的位置 依旧能够保证程序打印出争取的值
var c = make(chan int)
var a string
func f() {
a = "hello, world" //1
<-c // 2
}
func main() {
go f()
c <- 0 //3
print(a) //4
}
以上例子依旧能够保证输出是hello world
因为 1 happens before
2 , 3 happens before
4, 又 2 happens before
3 (无缓冲channel 消息发送者会一直阻塞到接收者接收消息) 所以 1 happens before
4。
假如channel 是一个缓冲的通道(e.g. c=make(chan int,1)) 那么程序将无法保证打印出hello world
(程序可能打印空字符串,崩溃 ,或者其他行为)
在有缓冲大小为C的通道中第k个接收者 heppens before
第 k+C 个发送者
锁
在sync
包实现了两种锁sync.Mutex
和sync.RWMutex
对于任何锁任何变量l 和n<m, 调用n 的 l.Unlock() happens before m的 l.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)
}
这也是能保证打印出hello world
的
ref: https://golang.org/ref/mem