Go 内存模型
在Go的官网有详细的文章介绍Go的内存模型,主要是从Happens-Before,同步可见性等几个方面
一、Happens Before
在单一gotoutine的情况下,编译器会在保证不影响最终结果的前提下,对代码进行重排序,但是在多goroutine的情况下就不这么乐观,需要一些规则来保证变量被正确的访问,所以整理一下缘由就是:
- 因为编译器执行时需要进行优化需求
- 所以需要对代码指令进行重排序
- 但是重排序不能影响到代码最终的结果,所以在重排序的时候就需要遵守一定的规则,这个规则就是Happens-Before原则
那么Go语言中的Happens-Before原则主要是为了保证在多goroutine的环境下读取共享数据的正确性,在多个内存操作执行之间的一种偏序关系。
在单goroutine环境下,Happens-Before表达的其实就是代码的执行顺序,在满足如下两个条件的时候,针对同一个变量的写操作(w)的结果对读操作(r)是可见的:
- r操作没有在w操作之前发生
- 在读操作r之前,写操作w之后没有其他的写操作(w1,w2,w3...)对变量进行了修改
满足了这两个条件,那么w的结果是对r可见的。这里是强调特指的w操作对r可见,当然这里可以存在其他的w1,w2等等,但是这里强调的是w的结果对r可见,那么在存在其他写操作的情况下,怎么保证w的结果对r可见呢?
- w操作比r操作提前发生
- 针对同一个共享变量的写操作(w1,w2,w3....)都要早于w或者晚于r
当然第二个限制会比第一个要严格一些,这里包括了并发的场景,不仅仅限制了当前goroutine写不能存在其他写操作,而且限制了其他goroutine下也不能存在其他写操作针对同一个共享变量进行并行操作。但是这里只是定了一个偏向关系,在多gouroutine的环境下,这个偏序关系并不能保证共享变量被正确处理,只有通过同步的方式来保证共享变量被正确的读取和写入。
还有需要注意的是在go内存模型中将多个goroutine中用到的全局变量初始化为它的类型零值在内被视为一次写操作,另外当读取一个类型大小比机器字长大的变量的值时候表现为是对多个机器字的多次读取,这个行为是未知的,go中使用sync/atomic包中的Load和Store操作可以解决这个问题,
二、同步
2.1 初始化
Go程序由一个goroutine启动,但是在这个goroutine中可以创建其他goroutine并发的运行。那么在这些并发运行的goroutine中也存在一种Happens-Before关系。
- 如果在P包中import了Q包,那么所有在Q包中定义的init方法都要优先与P包中任何方法前执行
- main包中的main方法要在所有的init方法执行完之后才执行
2.2 创建goroutine
通过go命令创建一个新的goroutine的动作,要早于该goroutine被执行(其实我觉得这个就是个废话)
2.3 销毁goroutine
退出一个goroutine的动作不一定在任何事件之前发生
var a string func hello() { go func() { a = "hello" }() print(a) }
这里在goroutine中对a变量进行赋值,但是没有添加任何同步原语,所以并不一定保证在执行print的时候a被正确的赋值,所以输出结果有可能是hello,也可能是空
2.4 Channel通信
通过channel进行通信,是在多个goroutine中进行同步的主要方式。在多个goroutine中,每个对通道进行写操作的goroutine都对应着一个从通道读操作的goroutine。当然在这里也存在一个Happens-Before关系:
- 向一个有缓冲通道中写消息的操作要早于接收消息的操作
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 的,因为对c通道的写操作一定是要早于读操作被执行,而对a的赋值是在向通道中写数据之前
- 有缓冲通道的关闭动作要早于从通道中读取的操作,对应的读取操作将会获取到一个对应类型的零值
package main var c = make(chan int, 10) var a string func f() { a = "hello, world" close(c) } func main() { go f() <-c print(a) }
这里依然可以保证会输出 hello, world ,因为close操作一定会早于从通道中读取数据,所以也就一定早于print操作
- 从无缓冲的通道中读取操作要早于向通道中写数据的操作
var c = make(chan int) var a string func f() { a = "hello, world" <-c } func main() { go f() c <- 0 print(a) }
这里也一定会保证会输出hello, world , 因为从通道中读取操作(<-c) 要早于写操作 (c <- 0) ,而对a的赋值操作要早与通道的读操作,所以在执行print函数的时候,一定能够确保a变量已经被成功赋值
- 从容量为C的通道接受第K个元素要早于向通道中写入第(K + C)个元素之前 ( 对有无缓冲的通道都适用 )
其实这里可以简答的理解:就是只有在通道满了的情况下,读操作才能获取向通道中写入的第一个元素。想想这是一定的啊,缓冲区不满,无法从通道中获取元素,
var limit = make(chan int, 3) func main() { for _, w := range work { go func(w func()) { limit <- 1 w() <-limit }(w) } select{} }
这里创建了一个3个缓冲区的通道,每次启动一个goroutine,然后从通道中读取一个元素,然后执行w方法,在执行完成之后从通道中获取一个元素,那么在通道没有满的情况下,goroutine是可以被正常执行的,但是如果通道满了的话,那么新的goroutine就会被阻塞。
三、锁
在sync包中提供两种类型的锁:
- sync.Mutex
sync.RWMutex
针对锁也存在Happens-Before的原则在:
- 对于这两种锁类型,在n < m的前提下,调用n次Unlock的操作,要早于调用m次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 ,因为对变量a的赋值操作是在第一次unlock操作之前,而unlock操作要早于lock操作,所以在执行print的时候能够确保a变量已经被正确赋值了
四、Once
Go中提供的保证只进行一次语义执行的工具类,同时也对应着一个Happens-Before规则:
- Once.Do执行的方法在多goroutine中只会被执行一次
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() }
五、不正确的同步
案例1
var a, b int func f() { a = 1 b = 2 } func g() { print(b) print(a) } func main() { go f() g() }
这段代码中可能先打印2,然后在打印0。因为在函数f中没有任何同步的措施,所以对a 和 b 的赋值操作是能存在重排序的,而且在main中通过关键字go启动了一个goroutine来执行函数f,所以f 和 g是并发运行,所以也无法保证两个goroutine的运行关系
案例2
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() }
这里通过双重检查机制来避免同步带来的开销,但是在setup中对变量a 和 done 的赋值顺序是无法保证的,比如第一个goroutine执行了setup函数,但是因为指令重排序,先对done赋值,那么第二个goroutine在执行到if判断的时候,就直接跳过once.Do方法,认为setup方法已经被执行过一次了,所以执行执行输出a , 那么这个时候如果a变量没有被正常初始化,那最终就会输出空值
案例3
var a string var done bool func setup() { a = "hello, world" done = true } func main() { go setup() for !done { } print(a) }
这里是因为在setup中无法保证变量的可见性是同步的,也就是在done被正确赋值的时候,无法保证a变量也被正确的赋值,所以最终的输出结果可能是 hello world,也可能是空
同样下面的代码也存在上面的这个问题
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) }
这里的问题发生在setup函数中的赋值,即使在main函数中能够看到 g==t , 然后在main中推出循环去执行print函数,但是这个时候也无法保证msg变量被正确的赋值,所以最终输出的结果就可能是空

浙公网安备 33010602011771号