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语言中是非常重要的,在读写锁、单例模式等领域应用广泛。