详解Go内存模型
介绍
Go内存模型规定了一些条件,在这些条件下,在一个goroutine中读取变量返回的值能够确保是另一个goroutine中对该变量写入的值。【翻译这篇文章花费了我3个半小时】
HappensBefore(在…之前发生)
在一个goroutine中,读操作和写操作必须表现地就好像它们是按照程序中指定的顺序执行的。这是因为,在一个goroutine中编译器和处理器可能重新安排读和写操作的执行顺序(只要这种乱序执行不改变这个goroutine中在语言规范中定义的行为)。
因为乱序执行的存在,一个goroutine观察到的执行顺序可能与另一个goroutine观察到的执行顺序不同。比如,如果一个goroutine执行a=1;b=2;,另一个goroutine可能观察到b的值在a之前更新。
为了规定读取和写入的必要条件,我们定义了happensbefore(在…之前发生),一个在Go程序中执行内存操作的部分顺序。如果事件e1发生在事件e2之前,那么我们说e2发生在e1之后。同样,如果e1不在e2之前发生也不在e2之后发生,那么我们说e1和e2同时发生。
在一个单独的goroutine中,happens-before顺序就是在程序中的顺序。
一个对变量v的读操作r可以被允许观察到一个对v的写操作w,如果下列条件同时满足:
r不在w之前发生在w之后,r之前,没有其他对v的写入操作w'发生。
为了确保一个对变量v的读操作r观察到一个对v的写操作w,必须确保w是唯一的r允许的写操作。就是说下列条件必须同时满足:
w在r之前发生任何其他对共享的变量v的写操作发生在w之前或r之后。
这两个条件比前面两个条件要严格,它要求不能有另外的写操作与w或r同时发生。
在一个单独的goroutine中,没有并发存在,所以这两种定义是等价的:一个读操作r观察到的是最近对v的写入操作w。当多个goroutine访问一个共享的变量v时,它们必须使用同步的事件来建立happens-before条件来确保读操作观察到预期的写操作。
在内存模型中,使用零值初始化一个变量的v的行为和写操作的行为一样。
读取和写入超过单个机器字【32位或64位】大小的值的行为和多个无序地操作单个机器字的行为一样。
同步
初始化
程序初始化操作在一个单独的goroutine中运行,但是这个goroutine可能创建其他并发执行的goroutines。
如果包p导入了包q,那么q的init函数执行完成发生在p的任何init函数执行之前。
函数main.main【也就是main函数】的执行发生在所有的init函数完成之后。
Goroutine创建
启动一个新的goroutine的go语句的执行在这个goroutine开始执行前发生。
比如,在这个程序中:
varastring funcf(){ print(a)//后 } funchello(){ a="hello,world" gof()//先 }
调用hello函数将会在之后的某个事件点打印出“hello,world”。【因为a=“hello,world”语句在gof()语句之前执行,而goroutine执行的函数f在gof()语句之后执行,a的值已经初始化了】
Goroutine销毁
goroutine的退出不保证发生在程序中的任何事件之前。比如,在这个程序中:
varastring funchello(){ gofunc(){a="hello"}() print(a) }
a的赋值之后没有跟随任何同步事件,所以不能保证其他的goroutine能够观察到赋值操作。事实上,一个激进的编译器可能删除掉整个go语句。
如果在一个goroutine中赋值的效果必须被另一个goroutine观察到,那么使用锁或者管道通信这样的同步机制来建立一个相对的顺序。
管道通信
管道通信是在goroutine间同步的主要方法。一个管道的发送操作匹配【对应】一个管道的接收操作(通常在另一个goroutine中)。
一个在有缓冲的管道上的发送操作在相应的接收操作完成之前发生。
这个程序:
varc=make(chanint,10)//有缓冲的管道 varastring funcf(){ a="hello,world" c<-0//发送操作,先 } funcmain(){ gof() <-c//接收操作,后 print(a) }
能够确保输出“hello,world”。因为对a的赋值操作在发送操作前完成,而接收操作在发送操作之后完成。
关闭一个管道发生在从管道接收一个零值之前。
在之前的例子中,将c<-0语句替换成close(c)效果是一样的。
一个在无缓冲的管道上的接收操作在相应的发送操作完成之前发生。
这个程序(和上面一样,使用无缓冲的管道,调换了发送和接收操作):
varc=make(chanint)//无缓冲的管道 varastring funcf(){ a="hello,world" <-c//接收操作,先 } funcmain(){ gof() c<-0//发送操作,后 print(a) }
也会确保输出“hello,world”。
如果管道是由缓冲的(比如,c=make(chanint,1))那么程序不能够确保输出"hello,world".(它可能会打印出空字符串、或者崩溃、或者做其他的事)
在一个容量为C的管道上的第k个接收操作在第k+C个发送操作完成之前发生。
该规则将前一个规则推广到带缓冲的管道。它允许使用带缓冲的管道实现计数信号量模型:管道中的元素数量对应于正在被使用的数量【信号量的计数】,管道的容量对应于同时使用的最大数量,发送一个元素获取信号量,接收一个元素释放信号量。这是一个限制并发的常见用法。
下面的程序对工作列表中的每一项启动一个goroutine处理,但是使用limit管道来确保同一时间内只有3个工作函数在运行。
varlimit=make(chanint,3) funcmain(){ for_,w:=rangework{ gofunc(wfunc()){ limit<-1//获取信号量 w() <-limit//释放信号量 }(w) } select{} }
锁
sync包实现了两个锁数据类型,sync.Mutex和sync.RWMutex。
对任何sync.Mutex或sync.RWMutex类型的变量l和n<m,第n个l.Unlock()操作在第m个l.Lock()操作返回之前发生。
这个程序:
varlsync.Mutex varastring funcf(){ a="hello,world" l.Unlock()//第一个Unlock操作,先 } funcmain(){ l.Lock() gof() l.Lock()//第二个Lock操作,后 print(a) }
保证会打印出"hello,world"。
Once
sync包提供了Once类型,为存在多个goroutine时的初始化提供了一种安全的机制。多个线程可以为特定的f执行一次once.Do(f),但是只有一个会运行f(),其他的调用将会阻塞直到f()返回。
一个从once.Do(f)调用的f()的返回在任何once.Do(f)返回之前发生。
在这个程序中:
varastring varoncesync.Once funcsetup(){ a="hello,world"//先 } funcdoprint(){ once.Do(setup) print(a)//后 } functwoprint(){ godoprint() godoprint() }
调用twoprint只会调用setup一次。setup函数在调用print函数之前完成。结果将会打印两次"hello,world"。
不正确的同步
注意到一个读操作r可能观察到与它同时发生的写操作w写入的值。当这种情况发生时,那也不能确保在r之后发生的读操作能够观察到在w之前发生的写操作。
在这个程序中:
vara,bint funcf(){ a=1 b=2 } funcg(){ print(b) print(a) } funcmain(){ gof() g() }
可能会发生函数g输出2然后0的情况。【b的值输出为2,说明已经观察到了b的写入操作。但是之后读取a的值却为0,说明没有观察到b写入之前的a写入操作!不能以为b的值是2,那么a的值就一定是1!】
这个事实使一些常见的处理逻辑无效。
比如,为了避免锁带来的开销,twoprint那个程序可能会被不正确地写成:
varastring vardonebool funcsetup(){ a="hello,world" done=true } funcdoprint(){ if!done{//不正确! once.Do(setup) } print(a) } functwoprint(){ godoprint() godoprint() }
这样写不能保证在doprint中观察到了对done的写入。这个版本可能会不正确地输出空串。
另一个不正确的代码逻辑是循环等待一个值改变:
varastring vardonebool funcsetup(){ a="hello,world" done=true } funcmain(){ gosetup() for!done{//不正确! } print(a) }
和之前一样,在main中,观察到了对done的写入并不意味着观察到了对a的写入,所以这个程序可能也会打印一个空串。更糟糕的是,不能够保证对done的写入会被main观察到,因为两个线程间没有同步事件。在main中的循环不能确保会完成。
类似的程序如下:
typeTstruct{ msgstring } varg*T funcsetup(){ t:=new(T) t.msg="hello,world" g=t } funcmain(){ gosetup() forg==nil{//不正确 } print(g.msg) }
即使main观察到了g!=nil,退出了循环,也不能确保它观察到了g.msg的初始值。
在所有这些例子中,解决方法都是相同的:使用显示地同步。
到此这篇关于Go内存模型的文章就介绍到这了,更多相关Go内存模型内容请搜索毛票票以前的文章或继续浏览下面的相关文章希望大家以后多多支持毛票票!