本文介绍Golang的内存模型相关知识。
什么是go的内存模型
指定了一系列条件,在这些条件下,可以保证在协程中对变量的读取操作可以观察到其他协程对同一变量写操作的结果,这就是go内存模型。
为什么需要这些条件?
因为编译器无法保证指令执行顺序与程序书写顺序一致。
如下示例:
1 | package main |
多运行几次,会发现结果可能不同,但可以发现,部分Println
打印的i
变量依然是5
,说明未能立观察到协程对i
变量的写操作。
所以我们为了保证协程间的变量读取的可观察,就需要建立Happen Before
关系的同步事件。也就是内存模型设定的条件
。
Happen Before
以下几种方式,可以建立Happen Before
关系的同步事件:
1 | 1.init函数 |
init函数
包a引入包b,那么包b的init就会happen before 包a的init函数。如下:
1 | // a.go |
1 | // b.go |
输出如下:
1 | b |
创建/销毁Goroutine
- 创建goroutine happen before goroutine执行
- goroutine执行 happen before goroutine销毁
channel
对于无缓冲的channel,
recv
操作happen beforesend
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15package main
import "fmt"
import "time"
func main() {
i := make(chan int)
go func() {
fmt.Println("send")
i <- 10
}()
go func() {
fmt.Println("recv")
fmt.Println(<-i)
}()
time.Sleep(time.Second)
}输出结果:
1
2
3send
recv
10关闭channel的操作 happen before 接受到0值。
对于容量为m的channel,第n次
recv
是happen before 第n+m次send
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23package main
import "fmt"
import "time"
func main() {
ch := make(chan int, 5)
go func() {
for i:=0;i<10;i++ {
time.Sleep(100)
fmt.Println("send", i)
ch <- i
}
close(ch)
}()
go func() {
for i := range ch {
time.Sleep(100)
fmt.Println("receive", i)
}
}()
time.Sleep(time.Second * 2)
}输出结果发现,对于长度为5的channel,第1次接收比6次发送要先执行。
锁
- 对于任意的
sync.Mutex
或者sync.RWMutex
,n<m时,n次Unlock
的调用happen beforeLock
的调用。 - 对于sync.RWMutex,第n次
Unlock
happen before第n次RLock
,而第n次RUnlock
又是happen before第n+1次Lock
sync.Once
- 对于
fn()
的单个调用会happen before所有的once.Do(fn)
返回之前发生执行结果:1
2
3
4
5
6
7
8
9
10
11
12
13package main
import "fmt"
import "sync"
func main() {
var o sync.Once
i := 5
for j := 0;j<3;j++ {
o.Do(func() {
i += 1
})
fmt.Println(i)
}
}1
2
36
6
6
备注
虽然这些都是go开发中一般都知道的常识
,但是我们还是需要了解,为什么会这样,为什么需要这样?
- 本文作者: Hongker
- 本文链接: https://hongker.github.io/2021/04/11/golang-mem/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!