Go并发编程-内存模型

一、前言

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执行代码4和5,然后在执行了goroutineA的代码(1),则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操作可以解决这个问题。

三、同步(Synchronization)

3.1初始化(Initialization)

程序的初始化是发生在一个goroutine内的,这个goroutine可以创建多个新的goroutine,创建的goroutine和当前的goroutine可以并发的运行。

如果在一个goroutine所在的源码包p里面通过import命令导入了包q,那么q包里面go文件的初始化方法的执行会happens before 于包p里面的初始化方法执行:

package main
import (
    "fmt"
    "main/hello"
)
func init() {
    fmt.Println("--main thread init---")
}
func main() {
    fmt.Println("---main func start----")
    hello.SayHello()
}
  • 如上代码main包里面导入了main/hello包,后者里面含有一个hello.go的文件,内容如下:

package hello
import (
    "fmt"
)
func init() {
    fmt.Println("--hello pkg init---")
}
func SayHello() {
    fmt.Println("--hello jiaduo---")
}
  • main包的main里面调用了包hello的SayHello 方法。

运行上面代码会输出:

--hello pkg init---
--main thread init---
---main func start----
--hello jiaduo---

可知hello包的init方法happen before main包的init执行,main包的init方法happen berfore main函数执行。

3.2 创建goroutine(Goroutine creation)

go语句启动一个新的goroutine的动作 happen before 该新goroutine的运行,例如下面程序:

package main
import (
    "fmt"
    "sync"
)
var a string
var wg sync.WaitGroup
func f() {
    fmt.Print(a)
    wg.Done()
}
func hello() {
    a = "hello, world"
    go f()
}
func main() {
    wg.Add(1)
    hello()
    wg.Wait()
}

如上代码调用hello方法后肯定会输出"hello,world",虽然可能等hello方法执行完毕后才输出(由于调度的原因)。

3.3 销毁goroutine(Goroutine destruction)

一个goroutine的销毁操作并不能确保 happen before 程序中的任何事件,比如下面例子:

var a string
func hello() {
    go func() { a = "hello" }()
    print(a)
}

如上代码 goroutine内对变量a的赋值并没有加任何同步措施,所以并能不保证print函数所在的goroutine对变量a的赋值可见。如果要确保一个goroutine对变量的修改对其他goroutine可见,必须使用一定的同步机制,比如锁、通道来建立对同一个变量读写的偏序关系。

四、创建的变量是在栈还是堆上分配?

首先从程序运行是否正确的角度来看,用户根本不需要知道变量是在堆还是栈上分配。在Go中只要某个变量还被引用,那么这个变量就一直存在,另外变量具体存放到哪里与go语言的语义无关,只是跟选择实现有关。

但是变量的存储位置确实会影响程序执行的效率,如果可能的话,go编译器会把在函数内创建的本地变量分配到该函数所在的栈帧上,但是如果编译器无法知道当前函数执行完毕后,其他地方是否还有对该变量的引用,编译器就会把该变量分配到堆上,以避免空指针异常,另外如果本地变量占用空间比较大,将他分配到堆上可能显得比分配到栈上更有意义。

在go当前的编译器中,如果其他变量持有当前变量的地址,则该当前变量优先在堆上分配,但是一些基本的编译器分析可以发现在某些情况这些变量的引用不会超过函数的返回,这时候当前变量是可以在栈上分配的。

五、总结

解决多goroutine下共享数据可见性问题的方法是在访问共享数据时候施加一定的同步措施,比如后面章节要讲的sync包下的锁和通道。

其中锁用来解决多个goroutine并发访问共享内存变量时候实现内存操作同步,这类似java的内存模型(多线程以共享内存来通信);而go也提供了其独特的通道同步措施,其倡导多线程以通信的方式来共享内存。

在后面我们讲解完锁后会有一节专门讲解锁操作的happen-before语义,然后讲解完通道后会专门讲解通道的happen-before语义。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值