Go的内存模型:如何保证并发读写的顺序

针对场景

Go的内存模型就是向广大程序员提供一种保证,保证多个goroutine访问同一个变量时,按照预定的顺序串行执行

重排、可见性问题

由于编译器、CPU的指令重排的存在,可能导致程序没有按照我们写的顺序执行出现可见性问题,此时就需要通过Go的内存屏障来保证执行顺序

Go内存屏障中有一个很重要的概念:happens-before,如果某些操作满足happens-before关系,那么就可以保证这些操作的先后顺序

import "time"

var a, b int

func write() {
	a = 1
	b = 2
}

func read() {
	println(b)
	println(a)
}

func main() {
	go write()
	go read()
	time.Sleep(1 * time.Second)
}

可见性问题:b打印为2时,a打印可能是0,因为这个程序没办法保证a=1在println(a)前

happens-before

a、b两个操作如果满足a happens-before b,那么a操作一定在b操作前执行

如果要保证对变量的读操作r,能够观察到一个对变量的写操作w,那么要保证两点:

  1. w happens-before v
  2. 其他的写操作(w1, w2, w3…)要么happens-before w,要么happens-after r

Go提供的happens-before

单goroutine

单个goroutine内部,代码编写的顺序和happens-before关系一致

init函数

单个包的happens-before

  1. 包级别变量的初始化happens-before init函数
  2. 包级别变量按照声明的顺序初始化,除非初始化时依赖后面声明的变量
    每个文件只能有一个init函数,init函数按照文件名顺序执行

多个包的happens-before

  1. 如果包p import 包q,那么包q的init函数 happens-before包p的任何初始化代码

在这里插入图片描述

go关键字

go语句的执行 happens-before goroutine中代码的执行

var x string

func hello() {
	x = "hello gopher"
	go func() {
		println(x)
	}()
}

x的写 happens-before go语句执行,go语句执行 happens-before println的执行,所以打印出来的一定是"hello gopher"

Channel

Channel提供的happens-before有四条规则:

对于buffered chan,第N次send happens-before 第N次recv的完成

对于unbuffered chan,recv happens-before send的完成

close一个Chan happens-before 从这个Chan读出一个零值

如果chan的容量是m,那么第n个recv happens-before 第n+m个send的完成

Mutex/RWMutex

Mutex、RWMutex提供的happens-before有三条原则:

第n次Unlock happens-before 第n+1次Lock

对于RWMutex,如果第n次的Lock调用返回了,那么第n次Unlock happens-before 任何一个RLock方法调用的返回

对于RWMutex,如果第n次的RLock调用返回了,那么第k次(k <= n)的RUnlock happens-before 任何一个Lock方法调用的返回

WaitGroup

对于一个WaitGroup,如果T0时刻计数值不为0,那么T0时刻后的wg.Add、wg.Done happens-before Wait方法调用的返回

Once

对于once.Do(f)的调用,f函数的单次调用 happens-before 任意次调用Do方法返回


版权声明:本文为qq_36059306原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。