52

Go 内存模型和Happens Before关系

 5 years ago
source link: https://studygolang.com/articles/14129?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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的程序员,那么你压根就不需要清楚这些,毕竟始终只有一个线程在跑是吧。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK