深入理解Golang内存模型

olzhy · · 443 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

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

有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

443 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传