Go 内存模型和Happens Before关系

hsiafan · · 3612 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

Happens Before 是内存模型中一个通用的概念,Go 中也定义了Happens Before以及各种发生Happens Before关系的操作,因为有了这些Happens Before操作的保证,我们写的多goroutine的程序才会按照我们期望的方式来工作。

什么是Happens Before关系

Happens Before定义了两个操作间的偏序关系,具有传递性。对于两个操作E1和E2:

  1. 如果E1 Happens Before E2, 则E2 Happens After E1;
  2. 如果E1 Happens E2, E2 Happens Before E3,则E1 Happens E3;
  3. 如果 E1 和 E2没有任何Happens Before关系,则说E1和E2 Happen Concurrently。

Happens Before的作用

Happens Before主要是用来保证内存操作的可见性。如果要保证E1的内存写操作能够被E2读到,那么需要满足:

  1. E1 Happens Before E2;
  2. 其他所有针对此内存的写操作,要么Happens Before E1,要么Happens After E2。也就是说不能存在其他的一个写操作E3,这个E3 Happens Concurrently E1/E2。

为什么需要定义Happens Before关系来保证内存操作的可见性呢?原因是没有限制的情况下,编译器和CPU使用的各种优化,会对此造成影响,具体的来说就是操作重排序和CPU CacheLine缓存同步:

  • 操作重排序。现代CPU通常是流水线架构,且具有多个核心,这样多条指令就可以同时执行。然而有时候出现一条指令需要等待之前指令的结果,或是其他造成指令执行需要延迟的情况。这个时候可以先执行下一条已经准备好的指令,以尽可能高效的利用CPU。操作重排序可以在两个阶段出现:
    • 编译器指令重排序
    • CPU乱序执行
  • CPU 多核心间独立Cache Line的同步问题。多核CPU通常有自己的一级缓存和二级缓存,访问缓存的数据很快。但是如果缓存没有同步到主存和其他核心的缓存,其他核心读取缓存就会读到过期的数据。

举例来说,看一个多Goroutine的程序:

// Sample Routine 1
func happensBeforeMulti(i int) {
	i += 2 // E1
	go func() { // G1 goroutine create
		fmt.Println(i) // E2
	}() // G2 goroutine destryo
}

对此来讲解:

  1. 如果编译器或者CPU进行了重排序,那么E1的指令可能在E2之后执行,从而输出错误的值;
  2. 变量i被CPU缓存到Cache Line中,E1对i的修改只改写了Cache Line,没有写回主存;而E2在另外的goroutine执行,如果和E1不是在同一个核上,那么E2输出的就是错误的值。

而Happens Before关系,就是对编译器和CPU的限制,禁止违反Happens Before关系的指令重排序及乱序执行行为,以及必要的情况下保证CacheLine的数据更新等。

Go 中定义的Happens Before保证

1) 单线程

  • 在单线程环境下,所有的表达式,按照代码中的先后顺序,具有Happens Before关系。

CPU和正确实现的编译器,对单线程情况下的Happens Before关系,都是有保障的。这并不是说编译器或者CPU不能做重排序,只要优化没有影响到Happens Before关系就是可以的。这个依据在于分析数据的依赖性,数据没有依赖的操作可以重排序。

比如以下程序:

// Sample Routine 2
func happsBefore(i int, j int) {
	i += 2             // E1
	j += 10            // E2
	fmt.Println(i + j) //E3
}

E1和E2之间,执行顺序是没有关系的,只要保证E3没有被乱序到E1和E2之前执行就可以。

2) Init 函数

  • 如果包P1中导入了包P2,则P2中的init函数Happens Before 所有P1中的操作
  • main函数Happens After 所有的init函数

3) Goroutine

  • Goroutine的创建Happens Before所有此Goroutine中的操作
  • Goroutine的销毁Happens After所有此Goroutine中的操作

我们上面提到的Sample Routine 1,按照规则1, E1 Happens before G1,按照本规则,G1 Happens Before E2,从而E1 Happens Before E2。

4) Channel

  • 对一个元素的send操作Happens Before对应的receive 完成操作
  • 对channel的close操作Happens Before receive 端的收到关闭通知操作
  • 对于Unbuffered Channel,对一个元素的receive 操作Happens Before对应的send完成操作
  • 对于Buffered Channel,假设Channel 的buffer 大小为C,那么对第k个元素的receive操作,Happens Before第k+C个send完成操作。可以看出上一条Unbuffered Channel规则就是这条规则C=0时的特例

首先注意这里面,send和send完成,这是两个事件,receive和receive完成也是两个事件。

然后,Buffered Channel这里有个坑,它的Happens Before保证比UnBuffered 弱,这个弱只在【在receive之前写,在send之后读】这种情况下有问题。而【在send之前写,在receive之后读】,这样用是没问题的,这也是我们通常写程序常用的模式,千万注意这里不要弄错!

// Channel routine 1
var c = make(chan int)
var a string

func f() {
	a = "hello, world"
	<-c
}
func main() {
	go f()
	c <- 0
	print(a)
}


// Channel routine 2
var c = make(chan int, 10)
var a string

func f() {
	a = "hello, world"
	<-c
}
func main() {
	go f()
	c <- 0
	print(a)
}


// Channel routine 3
var c = make(chan int, 10)
var a string

func f() {
	a = "hello, world"
	c <- 0
}

func main() {
	go f()
	<-c
	print(a)
}

比如上面这三个程序,使用channel来做同步,程序1和程序3是能够保证Happens Before关系的,程序2则不能够,也就是程序可能不会按照期望输出"hello, world"。

5) Lock

Go里面有Mutex和RWMutex两种锁,RWMutex除了支持互斥的Lock/Unlock,还支持共享的RLock/RUnlock。

  • 对于一个Mutex/RWMutex,设n < m,则第n个Unlock操作Happens Before第m个Lock操作。
  • 对于一个RWMutex,存在数值n,RLock操作Happens After 第n个UnLock,其对应的RUnLockHappens Before 第n+1个Lock操作。

简单理解就是这一次的Lock总是Happens After上一次的Unlock,读写锁的RLock HappensAfter上一次的UnLock,其对应的RUnlock Happens Before 下一次的Lock。

6) Once

once.Do中执行的操作,Happens Before 任何一个once.Do调用的返回


如果你对JVM的内存模型及定义的Happens Before关系都有所了解,那么这里对Go的内存模型的讲解与之非常类似,理解起来会非常容易。太阳底下无新鲜事,了解了一种语言的内存模型设计,其他类似的语言也就都可以很容易的理解了。如果是前端或者使用node的程序员,那么你压根就不需要清楚这些,毕竟始终只有一个线程在跑是吧。


有疑问加站长微信联系(非本文作者)

本文来自:知乎专栏

感谢作者:hsiafan

查看原文:Go 内存模型和Happens Before关系

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

3612 次点击  ∙  2 赞  
加入收藏 微博
被以下专栏收入,发现更多相似内容
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传