【Go并发编程】内存模型

267 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 23 天,点击查看活动详情

Go语言内存模型

Go语言内存模型(Go Memory Model,简称GMM)是Go语言用来描述多线程程序中,不同线程间访问内存的规则和约束。GMM规定了内存模型的原子性、可见性、顺序性等规则,保证了多线程程序的正确性。

在GMM中,每个变量都有一个所属的内存位置,不同的Goroutine可以对同一内存位置进行访问,GMM规定了对内存位置的读写操作都必须满足一定的原子性、可见性和顺序性。

具体来说,GMM规定了以下几个方面:

  1. 原子性:每个内存操作都是原子的,要么完全执行成功,要么完全不执行。
  2. 可见性:不同Goroutine对于相同内存位置的读写操作是可见的,即一个Goroutine对于内存的修改,另一个Goroutine能够立刻看到。
  3. 顺序性:不同内存操作的执行顺序不能随意调换,必须满足一定的顺序性。
  4. Happens-before关系:如果一个事件A happens-before另一个事件B,那么事件A中的所有操作对事件B都是可见的。

GMM保证了多线程程序的正确性,同时也给程序员带来了方便,可以更加灵活地编写并发程序。

指令重排

指令重排是指计算机在编译或运行过程中,为了提高执行效率,可能会重新安排指令执行的顺序。指令重排的目的是通过优化指令的执行顺序,减少空闲的 CPU 等待时间,提高计算机的运行效率。在多线程编程中,指令重排可能会导致程序出现不符合预期的行为。例如当两个 goroutine 同时对一个数据进行读写时,假设 goroutine g1 对这个变量进行写操作 w,goroutine g2 同时对这个变量进行读操作 r,那么,如果 g2 在执行读操作 r 的时候,已经看到了 g1 写操作 w 的结果,那么,也不意味着 g2 能看到在 w 之前的其它的写操作:

var a, b int

func f() {
   a = 1 // w之前的写操作
   b = 2 // 写操作w
}
func g() {
   print(b) // 读操作r
   print(a) // ???
}
func main() {
   go f() //g1
   g()    //g2
}

上面的代码可能会输出20。这些类似的现象是go语言不能提供保障,在某些cpu架构和Go编译器下可能会出现。

happens-before

事件 A 发生在事件 B 之前,我们可以说 A happens-before B。在 Go 中,以下情况会建立 happens-before 关系:

  • 在单个的 goroutine 内部, happens-before 的关系和代码编写的顺序是一致的
  • 如果包 p 导入了包 q,那么,q 的 init函数的执行一定 happens before p 的任何初始化代码。
  • main 函数一定在导入的包的 init 函数之后执行。
  • 一个变量的初始化 happens-before 对该变量的读或写操作;
  • 一个 channel 操作 happens-before 从该 channel 中读取数据的操作;
  • close 一个 Channel 的调用 happens before 从关闭的 Channel中读取出一个零值
  • 容量是 0 的 Channel,从此Channel 中读取数据的调用一定 happens before 往此 Channel 发送数据的调用完成
  • 如果 Channel 的容量是 m(m>0),那么,第 n 个 receive 一定happens before 第 n+m 个 send 的完成
  • 启动 goroutine 的 go 语句的执行,一定 happensbefore 此 goroutine 内的代码执行。
  • Mutex/RWMutex
    • 第 n 次的 m.Unlock 一定 happens before 第 n+1 m.Lock 方法的返回;
    • 对于读写锁 RWMutex m,如果它的第 n 个 m.Lock 方法的调用已返回,那么它的第 n个 m.Unlock 的方法调用一定 happens before 任何一个 m.RLock 方法调用的返回,只要这些 m.RLock 方法调用 happens after 第 n 次 m.Lock 的调用的返回。这就可以保证,只有释放了持有的写锁,那些等待的读请求才能请求到读锁。
    • 对于读写锁 RWMutex m,如果它的第 n 个 m.RLock 方法的调用已返回,那么它的第k (k<=n)个成功的 m.RUnlock 方法的返回一定 happens before 任意的m.RUnlockLock 方法调用,只要这些 m.Lock 方法调用 happens after 第 n 次m.RLock。
    • 读写锁的 Lock 必须等待既有的读锁释放后才能获取到
  • Wait 方法等到计数值归零之后才返回
  • Once 函数 f 一定会在 Do 方法返回之前执行
  • 一次函数调用的返回 happens-before 对该函数的下一次调用;
  • 在单个 goroutine 中,程序顺序规则(program order rule)确定了事件之间的 happens-before 关系。
  • Go 内存模型的官方文档并没有明确给出 atomic 的保证