白话Go内存模型Happen-Before

1. 内存模型介绍

内存模型是程序在内存中的行为规范,也就是说对于同一份代码,在不同的CPU架构和操作系统中运行时,内存模型可能有所不同。Go语言的内存模型定义了并发安全的操作对象,同时保证了操作的一致性和正确性。

Go语言的内存模型使用了Happen-Before关系。Happen-Before定义了内存操作之间的偏序关系,在Go代码中Happen-Before能够有效的保证了内存操作的可见性和正确性。Go语言的并发模型通过Goroutine+Channel实现,内存同步通过Happen-Before关系确保可见性和正确性。

2. Happen-Before关系

2.1 Happen-Before规则

Happen-Before是Go语言内存模型中的偏序关系,也是所有因果关系的基础。一组并发操作中,如果存在Happen-Before关系,那么这组操作的执行顺序就是确定的。由此,内存操作之间的关系也就被定义好了。

Go语言内存模型中Happen-Before定义了下列规则:

程序顺序规则(Program Order Rule)

同步规则(Synchronization Rule)

锁定规则(Locking Rule)

传递性规则(Transitivity Rule)

程序顺序规则(Program Order Rule)

程序顺序规则简称PO Rule,指的是程序中的内存操作按照程序码的顺序被执行,程序码中前一条指令Happen-Before其后一条指令。

同步规则(Synchronization Rule)

同步规则简称Sync Rule,指的是有一个线程对同一个变量做了unlock操作后,在另外一个线程中对同一个变量进行lock操作,那么这个unlock操作和lock操作之间的操作Happen-Before。

锁定规则(Locking Rule)

锁定规则简称Lock Rule,指的是Unlock操作Happen-Before后续的Lock操作。

传递性规则(Transitivity Rule)

传递性规则简称Transitivity Rule,指的是如果A Happen-Before B, B Happen-Before C, 那么A Happen-Before C。

2.2 Happen-Before实例分析

下面通过一个Happen-Before实例来加深理解:

// 1. main中的语句先于x = 1执行

var x int = 0

func f1() {

// 3. f1中,x = 1执行先于f2()执行

x = 1

}

func f2() {

// 4. f2中,fmt.Println()执行先于y = 1执行

fmt.Println(x)

y = 1

}

var y int = 0

func main() {

// 2. main中的fmt.Println()执行会在f1()执行之前发生

fmt.Println(x)

go f1()

go f2()

time.Sleep(time.Millisecond * 1000)

}

对于以上的代码,可以得到下面的图:

![Happen-Before实例](https://i.loli.net/2021/02/22/Atlgukw6YpE7Xiv.png)

通过图可以发现,对于 x = 1 的操作来说,它必须Happen-Before于 fmt.Println(x) 的操作,而 fmt.Println(x) 的操作又必须Happen-Before于 y = 1 的操作,所以我们可以知道 x = 1 Happen-Before y = 1 。

那么,对于以上的代码来说,竞争检测器会在运行时提醒我们:

$ go run -race example.go

```

WARNING: DATA RACE

Read at 0x00000132e4d8 by main goroutine:

main.main()

/WORKSPACE/example.go:16 +0x7b

Previous write at 0x00000132e4d8 by goroutine 7:

main.f1()

/WORKSPACE/example.go:8 +0x39

Goroutine 7 (running) created at:

main.main()

/WORKSPACE/example.go:14 +0x63

Goroutine 8 (running) created at:

main.main()

/WORKSPACE/example.go:15 +0x7b

```

从竞争检测器的结果来看,虽然没有panic执行的情况,但是在两个Goroutine之间还是存在数据竞争,这说明 select 语句并不能保证所有的goroutine是安全的。

3. Happen-Before的应用

3.1 Happen-Before的重要性

在Go语言中,Happen-Before关系非常重要,因为它保证了原子性、同步和可见性的正确性。特别当我们进行多线程编程的时候,考虑合理利用Happen-Before关系,将能够写出线程安全、高效的代码。在Happen-Before规则中,程序顺序规则、同步规则和锁定规则应用最为广泛。

3.2 Happen-Before的实际应用

Happen-Before关系在Go语言中被广泛应用,例如:

单例模式(Singleton)

package singleton

import (

"sync"

)

type single struct {

}

var (

instance *single

once sync.Once

)

func GetInstance() *single {

once.Do(func() { instance = &single{} })

return instance

}

Singleton模式被广泛应用于在多种框架中,它可能是一个非常重要的实例。而在Go语言中,Singleton模式可以通过一个sync.Once结构体实现,sync.Once保证给定函数只会被执行一次。

并发读写锁的使用

// 同一个Mutex互相不要嵌套包含,否则会造成死锁

import (

"fmt"

"sync"

)

// 正确的方式

func test1() {

// 基于Mutex的互斥

var mtx sync.Mutex

mtx.Lock()

defer mtx.Unlock()

fmt.Printf("test\n")

}

// 正确的方式 2,对于锁的使用可以直接使用下面的方式

func test2() {

var mtx sync.Mutex

mtx.Lock()

// ...

mtx.Unlock()

}

// 永远不要这样做!

func test3() {

var mtx sync.Mutex

mtx.Lock()

test4(&mtx)

mtx.Unlock()

}

func test4(mtx *sync.Mutex) {

mtx.Lock()

defer mtx.Unlock()

fmt.Printf("test\n")

}

在Go语言中,sync.Mutex被广泛地应用于互斥锁。对于一个Mutex来说,如果要进行解锁,那么它的锁定方法必须在它的解锁操作之前Happen-Before。否则,就会发生数据竞争。

4. 总结

内存模型是程序在内存中的行为规范,Go语言的内存模型定义了并发安全的操作对象,并且保证了操作的一致性、可见性和正确性。在Go语言中,内存同步通过Happen-Before关系来实现。Happen-Before定义了内存操作之间的偏序关系,它在Go代码中能够有效的保证了内存操作的可见性和正确性。Happen-Before关系在Go语言中是非常重要的,在读写锁、单例模式等领域应用广泛。

后端开发标签