Go内存模型
介绍
Go的内存模型指定了一个条件,在该条件下,可以保证在一个goroutine中读取便利,以观察通过写入不同goroutine中的同一变量的产生的值。
建议
修改同时被多个goroutine访问的数据的程序,必须序列化此类访问。 为了序列化此类访问,可以使用channel操作或者其他类似sync包或者sync/atomic包中的同步原语。
如果你读完本文的剩余部分以理解你的程序的行为,那你就太聪明了。
Happens Before
在一个goroutine中,读和写必须表现如下:他们被以程序指定的顺序执行。意思就是:编译器和处理器可以吧一个goroutine中的读和写操作的执行顺序重新排序,重新排序只会发生在重新排序不会改变那个goroutine中的行为,保持和go语言规范定义的哪有。例如,一个goroutine执行a=1;b=2; 另外一个观察到的b更新的值遭遇a更新的值。
为了规范读取和写入的邀请,我们定义了happens before,一个go程序中执行内存操作的部分顺序。 如果时间e1在e2之前发生,我们说e2发生在e1之后。同样的,如果e1没有发生在e2之前也没有发生在它之后,我们就说他们并发。
在单个goroutine中,happens-before顺序是通过程序表达的。
在满足下面的条件的情况下,一个读对变量v的读r是被允许观察一个对v的写操作w:
1 r没有在w之前发生
2 没有其他的对v的写操作w‘发生在w之后并在r之前。
为了保证变量v的读r观察到一个特殊的对v的写w,需要确保w是唯一一个读r可以观察到的写。也就是说满足下面条件的情况下,r可以观察到w
1 w在r之前发生
2 所有对变量v的写都没有在w之前发生 也没有在r之后发生
这两个条件比前两个要强。它要求没有其他鞋操作同时发生在w或者r之间。
在单个goroutine中,没有并发。所以两个定义等价:读r观察最近写入w对v写入的值。当在多个goroutine中,他们必须适应同步时间来建立happens before条件一确保读观察到想要的写。
有类型0只值的变量v的初始化表现为一个写操作,在内存模型中。
大于单个机器字的值的读取和写入表现为以未指定的顺序进行的多个机器字大小的操作。
同步
初始化
程序初始化运行在一个goroutine中,但是goroutine可以创建其他gorountine,他们并发执行。
如果一个包p导入包q,q的全部init函数都会在p包的init之前执行,main.main发送在所有init执行之后。
goroutine创建
go声明在goroutine执行之前启动一个goroutine,例如:
var a string
func f() {
print(a)
}
func hello() {
a = "hello, world"
go f()
}
hello调用将在未来的某个时间点(也许是hello返回之后)打印hello world
goroutine结构
goroutine的退出不保证在程序中的任何事件之前发生。例如,在此程序中:
var a string
func hello() {
go func() { a = "hello" }()
print(a)
}
对a的赋值没有跟随任何同步事件,因此不保证任何其他goroutine都能观察到它。实际上,一个积极的编译器可能会删除整个go语句。
如果必须由另一个goroutine观察到goroutine的影响,请使用锁定或通道通信等同步机制来建立相对排序。
通道通信
通道通信是goroutines之间同步的主要方法。特定频道上的每个发送都与该频道的相应接收相匹配,通常在不同的goroutine中。
通道上的发送在该通道的相应接收完成之前发生。
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的发送之前 通道的关闭happens before 返回0值的接受之前,因为通道被关闭了。
上面的例子中,用close(c)取代c<-0具有同样的效应。
从一个没有buffer的通道的接受happens before对这个通道的发送操作。
锁
sync包实现了两种锁类型 sync.Mutex, sync.RWMutex
对于人sync.Mutex或者sync.RWMutex类型l和n<m, 调用n的l.Unlock() happens before 的m l.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, 第一个调用l.Unlock(f函数中的调用)happens before 第二个l.Lock调用返回之前。
Once
当多个goroutine初始化时,sync包提供一个安全的初始化机制。多个线程可以执行once.Do(f),但是只有一个执行成功,其他调用将会阻塞,直到f执行完成。
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()
}
调用twoprint将调用设置一次。设置功能将在打印之前完成。结果将是“hello, world”将被打印两次。
网友评论