Golang 内存模型
1 指令顺序调整
对于单goroutine程序代码,编译器和处理器有时会调整源码中的指令顺序来做一些优化。当然,此类调整在当前gorouine程序来看并不会改变其源码指令所指定的行为。但在多个线程共享内存的情形下,某个goroutine内部的指令顺序调整可能会影响到依赖其指令顺序的其他goroutine的行为。
看一段代码:
package main import "fmt" var s string var done bool func setup() { s = "hello world" done = true if done { fmt.Println(s) } } func main() { go setup() for !done { } fmt.Println(s) }
如上代码,main函数等待setup将s赋值成功,期待打印出“hello world”。但main函数打印结果可能与预期不同,有可能打印为空串。原因在于该代码受编译器版本或运行时影响,即当前程序使用不同的编译器版本或在不同体系结构系统上运行时,结果可能不同。
原因在于如上代码中的setup函数的两行赋值语句指令顺序可能会被编译器或运行时CPU更改,即变为:
func setup() { done = true s = "hello world" if done { fmt.Println(s) } }
更改后,执行setup的goroutine本身的行为未受影响,其打印结果总会是“hello world”。
但依赖done变量写入的main函数goroutine的行为会受影响,其打印结果不一定是“hello world”。
2 Golang内存顺序保证
从如上例子可以看出,并发场景下,为保障程序逻辑正确性,需要想办法保障不同goroutine中的代码执行先后顺序。
不同的CPU体系结构提供不同的fence指令来防止指令顺序重排。而直接在代码中使用fence来作逻辑控制,抬高了并发编程的门槛。Golang并未内置直接操作CPU fence指令的函数或方法,而是提供了诸多“happens before”(先于)机制来保障程序执行顺序。
初始化顺序保证:
a)当前包所有包级变量初始化先与init函数执行;
b)依赖包init函数执行先于当前包包级变量初始化;
c)所有依赖包包级变量初始化与init函数执行均先于main函数执行。
所以一个包含依赖包的程序的初始化执行顺序为:
依赖包包级变量初始化 < 依赖包init函数执行 < 当前包包级变量初始化 < 当前包init函数执行(<表示先于)。
下面用一段代码证明上述初始化顺序。
在$GOPATH/src/github.com/p下有这样一段代码p.go:
package p import "fmt" var a = func() int { fmt.Println("variable init in p"); return 1 }() func init() { fmt.Println("p init") }
在$GOPATH/src/github.com/test下的main.go依赖了p包,代码如下:
package main import ( "fmt" _ "github.com/p" ) var b = func() int { fmt.Println("variable init in main"); return 2 }() func init() { fmt.Println("main init") } func main() { }
运行main.go,输出结果为:
variable init in p
p init
variable init in main
main init
goroutine创建顺序保证:
a)一个goroutine的创建先于其执行。
例如,如下代码:
var a, b string func f() { a = "hello" go func() { fmt.Println(a) b = "world" go func() { fmt.Println(b) }() }() }
f函数中,a的赋值先于fmt.Println(a);b的赋值先于fmt.Println(b),其打印结果总是:
hello
world
goroutine销毁(无顺序保证):
goroutine的销毁并未有先于程序任何事件点的保障。
请看如下代码:
var a string func f() { go func() { a = "hello" }() fmt.Println(a) }
在未加任何同步的情况下,a在一个goroutine是否赋值成功,对任何其他需要“observe”其值的goroutine是没有保证的。
所以若一个goroutine需要“observe”另一个goroutine,请使用同步机制(如锁)或使用Channel通信来保证执行顺序。
Channel通信顺序保证:
a)一个Channel的发送操作先于发送操作完成;
b)一个Channel的接收操作先于接收操作完成;
c)不论是Buffered Channel还是Unbuffered Channel,一个Channel的第N个成功发送先于第N个成功接收完成;
d)一个容量为M的Channel的第N个成功接收先于第N+M个成功发送完成(特别当M=0时,其为Unbuffered Channel,其第N个成功接收先于第N个成功发送完成);
e)一个Channel的关闭先于接收完成(Channel关闭时返回“零值”)。
看一段代码:
package main import "fmt" var a string func main() { done := make(chan bool, 3) go func() { a = "hello world" done <- true }() <-done fmt.Println(a) }
这段代码输出“hello world”是有保证的。因a的写入先于done的发送,done的发送先于done的接收完成,done的接收完成先于a的打印。
若将如上代码稍作改动,将done的发送改为done的close,其仍可以保证打印结果为“hello world”。
package main import "fmt" var a string func main() { done := make(chan bool, 3) go func() { a = "hello world" close(done) }() <-done fmt.Println(a) }
原因是,Channel的关闭要先于接收完成。
再看一段代码:
package main import "fmt" var a string func main() { done := make(chan bool) go func() { a = "hello world" <-done }() done <- true fmt.Println(a) }
这段代码将Channel通信中第一段代码的发送与接收互换位置,并改用Unbuffered Channel,仍可以保证打印结果为“hello world”。原因在于,a的写入先于done接收,done接收先于done发送完成,done发送完成先于a的打印。
上述规则中的规则d)可以使用Buffered Channel的容量作并发限制。
如下代码,在同一时刻至多有2个work()在执行。
package main import ( "fmt" "time" ) func main() { works := []func(){ func() { fmt.Println("working 0") }, func() { fmt.Println("working 1") }, func() { fmt.Println("working 2") }, func() { fmt.Println("working 3") }, func() { fmt.Println("working 4") }, func() { fmt.Println("working 5") }, func() { fmt.Println("working 6") }, func() { fmt.Println("working 7") }, } limit := make(chan int, 2) for _, work := range works { go func(func()) { limit <- 1 time.Sleep(time.Second) work() <-limit }(work) } select {} }
锁顺序保证:
a)对于sync.Mutex或sync.RWMutex变量l,第N个l.Unlock()调用先于第N+1个l.Lock()调用返回;
b)对于sync.RWMutex变量l,第N个l.Unlock()调用先于第N个l.RLock(),l.RUnlock()调用先于第N+M(M>=0)个l.Lock();
请看如下代码:
package main import ( "fmt" "sync" ) var a string var l sync.Mutex func main() { l.Lock() go func() { a = "hello world" l.Unlock() }() l.Lock() fmt.Println(a) }
可以保证其打印结果为“hello world”,因启动的goroutine中第一次l.Unlock()调用先于main中第二次l.Lock()调用返回。
接下来将Mutex改为RWMutex,代码如下:
package main import ( "fmt" "sync" ) var a string var l sync.RWMutex func main() { l.RLock() go func() { a = "hello world" l.RUnlock() }() l.Lock() fmt.Println(a) }
同样可以保证打印结果为“hello world”,因启动的goroutine中第一次l.RUnlock()调用先于main中第一次l.Lock()。
同理,若改为如下方式,同样可以保证打印结果。
package main import ( "fmt" "sync" ) var a string var l sync.RWMutex func main() { l.Lock() go func() { a = "hello world" l.Unlock() }() l.RLock() fmt.Println(a) }
因启动的goroutine中第一次l.Unlock()调用先于main中第一次l.RLock()。
sync.Once用来对多个goroutine同时调用某个函数时(once.Do(f)),保证仅有一个goroutine可以调用f(),其余goroutine的调用会阻塞直至f()返回。
如下代码,setup函数仅会执行一次。
package main import ( "fmt" "sync" "time" ) var a string var once sync.Once func setup() { fmt.Println("setup calling") a = "hello world" } func main() { for i := 0; i < 4; i++ { go func(i int) { once.Do(setup) fmt.Println(a, i) }(i) } time.Sleep(time.Second) }
输出结果为:
setup calling
hello world 2
hello world 3
hello world 0
hello world 1
额外注意:若main函数在新启goroutine时,未将i提取为函数参数,会发生多个goroutine重用最后i值的情况。
如如下代码所示:
func main() { for i := 0; i < 4; i++ { go func() { once.Do(setup) fmt.Println(a, i) }() } time.Sleep(time.Second) }
打印结果为:
setup calling
hello world 4
hello world 4
hello world 4
hello world 4
所以新启动程序时,宿主函数的参数使用要注意将其放入新的函数的参数列表或者在goroutine启动前使用原变量值的新实例。如如下代码所示。
func main() { for i := 0; i < 4; i++ { i := i go func() { fmt.Println(i) }() } ... }
Golang参考资料
[1] https://golang.org/ref/mem
[2] http://nil.csail.mit.edu/6.824/2016/notes/gomem.pdf
[3] https://go101.org/article/memory-model.html
[4] https://medium.com/@edwardpie/understanding-the-memory-model-of-golang-part-1-9814f95621b4
[5] https://medium.com/@edwardpie/understanding-the-memory-model-of-golang-part-2-972fe74372ba
有疑问加站长微信联系(非本文作者)