这城市风很大,孤独的人总是晚回家
1. 前言
golang的内存模型涉及的东西比较多,比如内存重排,L1-L3级别缓存以及编译器和处理器等,把这些东西都讲透是不现实的,那么我们接下来重点说下golang是如何解决Happens Before的。
2. Happens Before原则
在一个gouroutine中,读和写一定是按照程序中的顺序执行的。即编译器和处理器只有在不会改变这个goroutine的行为时才可能修改读和写的执行顺序。由于重排,不同的goroutine可能会看到不同的执行顺序。例如,一个goroutine执行a = 1;b = 2;,另一个goroutine可能看到b在a之前更新。 为了说明读和写的必要条件,我们定义了先行发生(Happens Before)--Go程序中执行内存操作的偏序。如果事件e1发生在e2前,我们可以说e2发生在e1后。如果e1不发生在e2前也不发生在e2后,我们就说e1和e2是并发的。 在单独的goroutine中先行发生的顺序即是程序中表达的顺序。 当下面条件满足时,对变量v的读操作r是被允许看到对v的写操作w的: 1 r不先行发生于w 2 在w后r前没有对v的其他写操作 为了保证对变量v的读操作r看到对v的写操作w,要确保w是r允许看到的唯一写操作。即当下面条件满足时,r 被保证看到w: 1 w先行发生于r 2 其他对共享变量v的写操作要么在w前,要么在r后。 这一对条件比前面的条件更严格,需要没有其他写操作与w或r并发发生。 单独的goroutine中没有并发,所以上面两个定义是相同的:读操作r看到最近一次的写操作w写入v的值。当多个goroutine访问共享变量v时,它们必须使用同步事件来建立先行发生这一条件来保证读操作能看到需要的写操作。 对变量v的零值初始化在内存模型中表现的与写操作相同。 对大于一个字的变量的读写操作表现的像以不确定顺序对多个一字大小的变量的操作。
3. 例子
创建goroutine:
a string func f() { print(a) } func hello() { a = "hello, world" go f() }
go关键字开启新的goroutine,a的w先行发生(Happens Before)于这个goroutine开始执行,之后某个时间打印出a的值hello,world即r操作。这个很明显满足我们的Happens Before原则。
销毁goroutine:
a string func hello() { go func() { a = "hello" }() print(a) }
gouroutine的退出并不会保证先行发生于程序的任何事件,即w和r的顺序没办法保证,那很有可能输出“”或者hello等,结果也就没办法确定了。
4. 解决
核心解决方案之一就是利用channel
package main var a string var c = make(chan int, 10) func hello() { go func() { a = "hello" c <- 0 //在channel的发送操作先行发生于对应的接收操作完成 }() <-c //这个等待上面c的写入完成 即保证下面的print(a)的r后于a的w,也即a的w先于a的r print(a) } func main() { hello() }
当然将c <- 0替换为close(c)还会产生同样的结果,即输出hello,这个大家可以去检测一下,眼过千遍不如手过一遍。
5. 小结
当然无缓冲的chan也能实现,只要按照HappensBefore的设计原则实现就行,解决方案不是唯一的。那么今天就到这里了,下期分享锁和once在解决内存模型中的作用。
6. 关注公众号
微信公众号:堆栈future