Go开发需要了解的一个内存模型细节转载本文请联系脑筋急转弯公众号。大家好,我是炸鱼。在我们的日常工作中,如果能够了解Go语言的内存模型,将会有很大的帮助。这样,在看一些极端的情况或者异常的面试题的时候,就可以了解到程序性能的很多根本原因。当然,用一篇普通的文章是不可能讲完Go内存模型的。所以今天的文章重点讲解Go语言happens-before原理的细节。让我们开始吸烟,炸鱼来揭开他的神秘面纱!内存模型的定义是什么?既然我们要理解happens-before原理,那么首先就要知道TheGoMemoryModel(Go内存模型)的定义是什么。官方的解释是这样的:Go内存模型规定了在一个goroutine中读取一个变量可以保证观察到在不同的goroutine中写入同一个变量所产生的值的条件。conditionsthatguaranteetoobservethevaluewritetothesamevariableindifferentgoroutines”。这是学习后续知识的大前提。whathappens-before是HappensBefore是一个专业术语,与Go语言没有直接关系,也就是说,它不是唯一的,通俗地说,它的定义是:在一个多线程程序中,假设有两个操作A和B,如果A发生在B之前(Ahappens-beforeB),那么A的影响onmemory将对执行B的线程可见。A不一定是happens-beforeB从happens-before的定义我们可以逆向思考。即:在同一个(同一个)线程中,如果两个操作A和B都执行了,A的语句一定在B之前,那么A一定发生在(happens-before)B之前。以下面的Go代码为例:varAintvarBintfuncmain(){A=B+1(1)B=1(2)}这段代码在同一个maingoroutine中,全局变量A在变量B之前声明。在main函数中,代码第(1)行,也在代码第(2)行之前。所以我们可以得出结论,(1)必须在(2)之前执行,对吧?答案是:错了,因为Ahappens-beforeB并不意味着A操作一定发生在B操作之前。实际上,在编译器中,上述代码在汇编中的实际执行顺序如下:0x000000000(main.go:7)MOVQ"".B(SB),AX0x000700007(main.go:7)INCQAX0x000a00010(main.go:7)MOVQAX,"".A(SB)0x001100017(main.go:8)MOVQ$1,"".B(SB)(2):加载B注册AX。(2):AssignB=1,在代码中执行为INCQ自增。(1):将寄存器AX中的值加1赋值给A。通过上面的分析我们可以知道。在代码行中(1)在(2)之前,但实际上(2)在(1)之前执行。那么这是否意味着违反了happens-before的设计原则呢?毕竟这是同一个线程中的操作,Go编译器有bug?其实不然,因为对A的赋值对对B的赋值本质上没有影响。所以不违反happens-before设计原则。Go语言中的happens-before在《The Go Memory Model》中,给出了Go语言中HappensBefore的清晰的语言定义。介绍中将使用以下术语:变量v:用于示例演示的指示变量。读r:代表读操作。Writew:表示写操作。定义允许对变量v的读取r观察到对v的写入w,前提是r不发生在w之前。在w之后但在r之前,没有其他w'写入v。为了保证变量v的读取r观察到对v的特定写入w,请确保w是唯一允许观察到的写入r。因此,如果以下两个条件都为真,则r一定会观察到w:w发生在r之前。对共享变量v的任何其他写入都发生在w之前或r之后。这似乎很尴尬。接下来,我们就以《The Go Memory Model》中的具体频道例子来进一步说明,让大家更好的理解。GoChannel实例在Go语言中提倡不通过共享内存进行通信;相反,通过通信共享内存:不要通过共享内存进行通信;相反,通过通信共享内存。因此,在Go项目中,Channel是一种非常常用的语法。原则上需要注意的是,通道上的发送发生在该通道上相应的接收完成之前。通道关闭发生在接收之前,返回零值,因为通道已关闭。无缓冲通道上的接收发生在该通道上的发送完成之前。在容量为C的信道上,第k次接收发生在信道的第k+C次发送完成之前。接下来,我们将根据这四个原则,一一举例,以供学习和理解。Example1GochannelExample1,你认为输出是什么。如下:varc=make(chanint,10)varastringfuncf(){a="炸鱼"(1)c<-0(2)}funcmain(){gof()<-c(3)print(a)(4)}答案是空字符串吗?程序最终的结果是正常输出“炸鱼”。原因如下:(1)happens-before(2)。(4)发生在(3)之后。当然,最后的(1)写变量a的操作必须发生在(4)打印方法之前,所以正确输出了“炸鱼”。可以满足“通道上的发送发生在该通道上相应的接收完成之前”。示例2主要是保证关闭管道时的行为。只需将前面示例中的c<-0替换为close(c)即可生成具有相同行为保证的程序。可以满足“通道的关闭发生在接收之前,因为通道关闭而返回零值”。Example3GochannelExample3,你认为输出会是什么。如下:varc=make(chanint)varastringfuncf(){a="炸鱼入脑"(1)<-c(2)}funcmain(){gof()c<-0(3)print(a)(4)}答案是空字符串吗?程序最终的结果是正常输出“炸鱼入脑”。原因如下:(2)happens-before(3)。(1)发生在(4)之前。可以满足“在该通道的发送完成之前发生无缓冲通道的接收”。如果我们把unbuffered改成make(chanint,1),即有缓冲的channel,就不能保证“炸鱼入脑”的正常输出了。Example4GochannelExample4,此程序为工作列表中的每个条目启动一个goroutine,但goroutine使用通道进行协调以确保一次最多只运行三个工作函数。代码如下:varlimit=make(chanint,3)funcmain(){for_,w:=rangework{gofunc(wfunc()){limit<-1w()<-limit}(w)}select{}}可以满足“在容量为C的信道上,第k次接收发生在该信道上第k+C次发送完成之前”。总结在本文中,我们对happens-before原则进行了基本解释。同时结合Go语言中实际的happens-before和happens-after场景进行展示和讲解。其实在日常的开发工作中,happens-before原则已经基本深入到潜意识中,就像设计模式一样。它会在不知不觉中应用,但是如果我们想要进一步学习和理解Go语言等内存模型,就必须对这个基本概念有所了解。你注意到这个问题了吗?欢迎留言讨论!参考TheGoMemoryModelGo内存模型&Happen-Before(一)GoLang内存模型Golanghappensbefore&channelGo内存模型
