一、前言
Go语言的内存模型规定了一个goroutine可以看到另外一个goroutine修改同一个变量的值的条件,这类似java内存模型中内存可见性问题。
当多个goroutine并发同时存取同一个数据时候必须把并发的存取的操作顺序化,在go中可以实现操作顺序化的工具有高级的通道(channel)通信和同步原语比如sync包中的Mutex(互斥锁)、RWMutex(读写锁)或者和sync/atomic中的原子操作。
二、Happens Before原则
当程序里面只有一个goroutine时候,虽然编译器和CPU由于开启了优化功能可能调整读写操作的顺序,但是这个调整是不会影响程序的执行正确性:
a := 1//1 b := 2//2 c := a + b //3 ...
如上代码由于编译器和cpu的优化,实际运行时候可能代码(2)先运行,然后代码(1)后执行,但是由于代码(3)依赖代码(1)和代码(2)创建的变量,所以代码(1)和(2)不会被放到代码(3)后运行,也就是说编译器和CPU在不改变程序正确性的前提下才会对指令进行重排序,所以上面代码在单一goroutine时候并不会存在问题,也就是在单一goroutine 中Happens Before所要表达的顺序就是程序执行的顺序。
但是在多个goroutine时候就可能存在问题,比如下面代码:
//变量b初始化为0 var b int //goroutine A go func() { a := 1 //1 b := 2 //2 c := a + b //3 }() //goroutine B go func() { if 2 == b {//4 fmt.Println(a)//5 } }()
- 如上代码变量b是一个全局变量,初始化为0值
- 下面开启了两个goroutine,假设goroutine B有机会输出值时候,那么它可能输出的值是多少那?其实可能是0也可能是1,输出1大家可能会感到很直观,那么为何会输出0 了?
- 这是因为编译器或者CPU可能会对goroutine A中的指令做重排序,可能先执行了代码(2),然后在执行了代码(1)。假设当goroutine A执行代码(2)后,调度器调度了goroutine B执行,则goroutine B这时候会输出0。
为了保证多goroutine下读取共享数据的正确性,go中引入happens before原则,即在go程序中定义了多个内存操作执行的一种偏序关系。如果操作e1先于e2发生,我们说e2 happens after e1,如果e1操作既不先于e2发生又不晚于e2发生,我们说e1操作与e2操作并发发生。
在单一goroutine 中Happens Before所要表达的顺序就是程序执行的顺序,happens before原则指出在单一goroutine 中当满足下面条件时候,对一个变量的写操作w1对读操作r1可见:
- 读操作r1没有发生在写操作w1前
- 在读操作r1之前,写操作w1之后没有其他的写操作w2对变量进行了修改
在一个goroutine里面,不存在并发,所以对变量的读操作r1总是对最近的一个写操作w1的内容可见,但是在多goroutine下则需要满足下面条件才能保证写操作w1对读操作r1可见:
- 写操作w1先于读操作r1
- 任何对变量的写操作w2要先于写操作w1或者晚于读操作r1
这两条条件相比第一组的两个条件更加严格,因为它要求没有任何写操作与w1或者读操作r1并发的运行,而是要求在w1操作前或读操作r1后发生。
在一个goroutine时候,不存在与w1或者r1并发的写操作,所以前面两种定义是等价的:一个读操作r1总是对最近的一个对写操作w1的内容可见。但是当有多个goroutines并发访问变量时候,就需要引入同步机制来建立happen-before条件来确保读操作r1对写操作w1写的内容可见。
需要注意的是在go内存模型中将多个goroutine中用到的全局变量初始化为它的类型零值在内被视为一次写操作,另外当读取一个类型大小比机器字长大的变量的值时候表现为是对多个机器字的多次读取,这个行为是未知的,go中使用sync/atomic包中的Load和Store操作可以解决这个问题。
解决多goroutine下共享数据可见性问题的方法是在访问共享数据时候施加一定的同步措施,比如sync包下的锁或者通道。
三、总结
happen-before规则定义了多个goroutine对同一个共享数据进行读写的偏序关系,把并发的操作变成了可以预见的顺序执行,这点和Java内存模型中的happen-before语义类似
原创文章,转载请注明: 转载自并发编程网 – ifeve.com本文链接地址: Go内存模型&Happen-Before(一)