Ref: https://golang.org/ref/memgolang
golang内存模型,主要说明了以下问题。在一个goroutine中读取变量,而该变量是由其余goroutine赋值的,这种状况下如何可以安全正确的读取。编程
对于有多个goroutine在使用的变量,修改时须要序列化的读取。缓存
主要方式包括,经过channel的方式、sync/atomic等原子同步操做等。安全
若是你想读完如下内容,以便理解你的程序内在运行机制,说明你很聪明。并发
可是不建议你这么聪明~编程语言
只有一个goroutine的时候,读写操做都会如程序定义的顺序执行。这是由于,尽管编译器和中央处理器是可能会改变执行顺序,但并不会影响编程语言定义的goroutine中的行为逻辑。但也是由于可能改变执行顺序,一样的操做在不一样的goroutine中观察到的执行顺序并不一致。好比A goroutine执行了a=1;b=2;另外一个goroutine观察到的结果多是b先于a被赋值了。函数
为具体说明读写操做的要求,咱们定义了以前某个版本中Golang程序中内存操做的一部分逻辑以下。若是事件e1发生在事件e2以前,咱们说e2发生在e1以后.若是e1并不发生在e2以前,也不发生e2在以后,咱们说e1和e2是同步发生的。atom
在一个单goroutine的程序中,事件发生的顺序就是程序描述的顺序。spa
知足以下两个条件的状况下,对变量v的读取操做r,是可以观察到对v的写入操做w的:线程
为了保证对变量v对读取操做r,可以观察到特定的对v得写操做w,须要保证w是r惟一可以观察到写操做。所以,要保证r可以观察到w须要知足以下两个条件:
这对条件是比第一个条件更加严格,它要求r和w的同时,没有其它的写操做(即和r或w同步的写操做);
在单一goroutine里面,没有同步操做,因此以上两组条件是等价的。可是对于多goroutine,须要经过同步事件来肯定顺序发生,从而保证读操做可以观察到写操做。
在内存模型里面,变量v初始化为零值,也是一种写操做。
读写大于单一机器码的变量的动做,实际操做顺序不定,
程序初始化在单一goroutine里面,可是goroutine会建立其余goroutine,而后多个goroutine同步执行。
若是package p引用了package q,q的init函数的执行,会先于全部p的函数执行。
Main.main函数的执行,在全部init函数执行完后。
go表达式,会建立一个goroutine,而后该goroutine才能开始执行。
var a string func f() { print(a) } func hello() { a = "hello, world" go f() }
以上代码示例,调用hello函数,可能在hello已经return到时候,f才回执行print。
goroutine的退出时机并无保证必定会在某个事件以前。
var a string
func hello() {
go func() { a = "hello" }()
print(a)
}
好比以上示例,对a的赋值,并不保证与hello自己的任何动做保持同步关系,因此也不能保证被其余任何goroutine的读操做观察到。事实上,任何一个激进的编译器都会把这里整个go表达式直接删掉,不作编译。
若是一个goroutine的影响想被其余的goroutine观察到,必须经过同步机制(好比锁、channel)来肯定相对顺序关系。
channel通讯是goroutines之间主要的同步方式。通常来讲channel上的每一次send都会相应有另外一个goroutine今后channel受到消息。
同一个channel上,send操做老是先于相应的receive操做完成。
var c = make(chan int, 10) var a string func f() { a = "hello, world" c <- 0 } func main() { go f() <-c print(a) }
以上示例,可以保证print出『hello, world』。对a的写,是先于往c中发送0,而从c中接收值,先于print。
channel的关闭,先于接收到该channel关闭而发出来的0.
在上面这个例子中,用close(c)代替 c<-0,其效果是同样的。
对于没有缓存的channel,receive发生在send完成以前。
var c = make(chan int) var a string func f() { a = "hello, world" <-c } func main() { go f() c <- 0 print(a) }
以上示例,依旧可以保证print出『hello, world』。对a的写,先于从c接收;从c接收,先于 c <- 0执行完; c <- 0执行完,先于print执行。
但若是channel是缓存的(例如c = make(chan int, 1)),那么以上程序不能保证print出『hello, world』,甚至有可能出现空值、crash等状况;
对于缓存容量为C的channel,第k次接收,先于K+C次发送完成。
这条规则归纳了缓存和非缓存channel的规则。所以基于带缓存的channel,能够实现令牌策略:在channel中缓存的数量表明active的数量;channel的缓存容量表示最大可使用的数量;发送消息表示申请了一个令牌,接收消息表示释放了一块令牌。这是限制并发经常使用的一种手段。
var limit = make(chan int, 3) func main() { for _, w := range work { go func(w func()) { limit <- 1 w() <-limit }(w) } select{} }
以上示例程序,对于work list中的每一条,都建立了一个goroutine,可是用limit这个带缓存的channel来限制了,最多同时只能有3个goroutines来执行work方法。
sync包中实现了两个锁的数据类型,分别是sync.Mutex和sync.RWMutex
对于任何的sync.Mutex和sync.RWMutex类型变量l,和n<m,对于l.Unlock()的调用n,老是先于对于l.Lock()的调用m。
var l sync.Mutex var a string func f() { a = "hello, world" l.Unlock() } func main() { l.Lock() go f() l.Lock() print(a) }
如上示例可以保证print出『hello, world』。f中第一个l.Unlock()的调用,先于main中第二个l.Lock()的调用;第二个l.Lock()的调用先于print的调用;
任何对于l.Rlock的调用(其中l为sync.RWMutex类型变量),老是有一个n,l.Lock在调用n执行l.Unlock以后才能return;对应的,l.RUnlock的执行在调用n+1执行l.Unlock以前。
Sync包提供了一种安全的多goroutine种初始化机制,那就是Once类型。对于特定的方法f,多个线程都能调用Once.Do(f),可是只有一个线程会执行f,其余线程的调用都会阻塞住,直到f执行完。
对于Once.Do(f),有且仅有一次调用会被真正执行,并且在其余被的调用返回以前执行完。
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() }
这里print两次『hello, world』,但只有第一次调用doprint会执行setup赋值。
对于同步发生的读操做r和写操做w,r有可能观察到w。但即便发生了这种状况,不表明r以后的读操做,也能观察到w以前的写操做。
var a, b int func f() { a = 1 b = 2 } func g() { print(b) print(a) } func main() { go f() g() }
如上例子,g打印出2,而后是0.这个事实颠覆了咱们的一些习惯认知。
对于同步问题加锁必定要double check。
var a string
var done bool
func setup() {
a = "hello, world"
done = true
}
func doprint() {
if !done {
once.Do(setup)
}
print(a)
}
func twoprint() {
go doprint()
go doprint()
}
如这个例子,不能保证观察到done的写操做时候,也能观察到对a的写操做。其中一个goroutine可能打印出空字符串。
另一种错误的典型以下:
var a string var done bool func setup() { a = "hello, world" done = true } func main() { go setup() for !done { } print(a) }
同上一个例子同样,这里对done得写观察,不能保证对a的写观察,因此也可能打印出空字符串。
更甚,因为main和setup两个线程间没有同步事件,并不能保证main中必定能观察到done的写操做,所以main中的一直循环下去没有结束。(这里不是很理解,只能说setup的执行时机和main中for循环没有明确的相对前后和相对距离,因此可能致使循环好久setup还没执行,或执行了可是没有更新到main所读取的done)
还有以上风格的一些变体,以下:
type T struct { msg string } var g *T func setup() { t := new(T) t.msg = "hello, world" g = t } func main() { go setup() for g == nil { } print(g.msg) }
即便main可以观察到g的赋值而退出循环,可是也不能保证观察到g.msg的初始化值。
对于以上全部例子,解决方案是同样的,定义明确的同步机制。