大家好,我是建宇。在我们的日常工作中,如果能够了解Go语言的内存模型,将会有很大的帮助。这样,在看一些极端的情况或者异常的面试题的时候,就可以了解到程序性能的很多根本原因。当然,用一篇普通的文章是不可能讲完Go内存模型的。所以今天的文章重点讲解Go语言happens-before原理的细节。开启吸力,炸鱼揭秘!内存模型的定义是什么?既然我们要理解happens-before原理,那么首先就要知道GoMemoryModel(Go内存模型)定义了什么。官方的解释是这样的:Go内存模型规定了在一个Goroutine中读取一个变量可以保证观察到在不同的goroutine中写入同一个变量所产生的值的条件。Go内存模型规定:“在一个goroutine中读取一个变量时,可以保证观察到在不同的goroutine中写入同一个变量的结果值”条件。这是学习后续知识的大前提。Whathappens-before是HappensBefore是一个与Go语言没有直接关系的技术术语,即它不是唯一的。通俗地说,它的定义是:在一个多线程程序中,假设有两个操作A和B,如果A发生在B之前(Ahappens-beforeB),那么A对内存的影响将会是可见的执行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="friedfish"(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语言等内存模型,就必须对这个基本概念有所了解。你有没有注意到这个问题?欢迎评论和讨论!如有任何问题,欢迎在评论区反馈交流。最好的关系是相互成就。您的好评是创作炸鱼最大的动力。感谢您的支持。文章持续更新中。可以微信搜索【脑补炸鱼】阅读。本文已收录在GitHubgithub.com/eddycjy/blog中。学习Go语言可以看Go学习地图和路线。欢迎星星提醒。参考TheGoMemoryModelGo内存模型&Happen-Before(一)GoLang内存模型Golanghappensbefore&channelGo内存模型
