Golang内存模型

Golang内存模型

参考自:https://golang.org/ref/mem

Golang的内存模型描述了这样的一种场景:在一个goroutine中对一个变量的读取能保证是由不同gorountine写入相同变量所产生的。

Happens Before

在单个goroutine中,只有在满足不改变语言规范所定义的行为时,编译器才能对单个goroutine所执行的读写进行重新排序。但由于重新排序,一个goroutine所观察到的执行顺序可能与另一个goroutine察觉到的执行顺序不同。

为了指定读写要求,在go程序中定义了一个叫Happens Before的偏序关系——如果事件e1发生在事件e2之前,那么我们说e2发生在e1之后。同样,如果e1不在e2之前发生并且在e2之后也没有发生,那么我们说e1和e2同时发生。

在单个goroutine中,Happens Before的顺序就是程序所表现出来的顺序。

为了保证对变量的读取R可以读取到由特定的对变量的写入W,即W是R可以观察到的唯一写入,必须要满足以下两个条件:

  1. W发生到R之前;
  2. 任何对变量的其他写入要么发生在w之前,要么发生在r之后;

变量的初始化为零值,其实也是内存模型中的零值写入。

Synchronization

初始化

程序的初始化是在单个goroutine中进行的,但goroutine可以创建其他goroutine,这是并发的。

如果一个package引入了另一个package,即被引入的package会先初始化。

main.main的开始必须要在所有init函数完成之后。

Goroutine的创建

以下面的为例子,f()打印出hello world可能会在hello()结束后才打印。

1
2
3
4
5
6
7
8
9
10
var a string

func f() {
print(a)
}

func hello() {
a = "hello, world"
go f()
}

Goroutine的销毁

无法保证goroutine的退出在程序中的任何其他事件发生之前发生。

1
2
3
4
5
6
var a string

func hello() {
go func() { a = "hello" }()
print(a)
}

对a的赋值很可能在下面的print中看不到,因为缺乏同步。

Channel的同步

通道通信是goroutine之间同步的主要方法。channel的发送必定发生在该channel接受完成之前。

1
2
3
4
5
6
7
8
9
10
11
12
13
var c = make(chan int, 10)
var a string

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

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

这样就能保证打印出hello, world。

另外,channel的关闭会发生在返回零值的接收之前,这样用close(c)替代c<-0也可以产生相同的保证行为。

而对于缺乏buffer的channel,其接收会发生在该channel的发送完成之前,例如这样也可以保证打印出正确的hello world,这里的发送和接收顺序与上面的例子相反。

1
2
3
4
5
6
7
8
9
10
11
12
13
var c = make(chan int)
var a string

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

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

但如果channel是带有buffer,就无法保证打印出hello world了。

在容量为C的通道上的第k个接收发生在该通道的第k + C个发送完成之前,因为不从channel接收数据就无法继续写入。

该规则将前一个规则推广到缓冲通道。 它允许通过缓冲的通道对计数信号量进行建模:channel中的项目数量对应于活动使用的数量,channel的容量对应于同时使用的最大数量,发送一个项目获取信号量,接收项目则会释放信号量。通过这种操作就可以限制其并发。

1
2
3
4
5
6
7
8
9
10
11
12
var limit = make(chan int, 3)

func main() {
for _, w := range work {
go func(w func()) {
limit <- 1
w() // 不处理完成,无法释放该信号量
<-limit
}(w)
}
select{}
}

Locks

sync包里实现了两种锁相关的数据类型:sync.Mutex和sync.RWMutex。通过锁的使用,我们可以在goroutine中保证同步。这样的一个程序就可以顺利打印出hello world。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var l sync.Mutex
var a string

func f() {
a = "hello, world"
l.Unlock()
}

func main() {
l.Lock()
go f()
l.Lock()
print(a)
}

Once

sync包还提供了一种初始化的安全机制,通过使用Once数据类型,多个线程都可以执行once.Do(f),但只有一个会运行f(),其他的调用将会block直接f()返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var a string
var once sync.Once

func setup() {
a = "hello, world"
}

func doprint() {
once.Do(setup)
print(a)
}

func twoprint() {
go doprint()
go doprint()
}

在这种机制下,a的赋值将会在打印之前执行。

Incorrect synchronization

需要注意的是读取R可能会观察到与R同时发生的写入W所写入的值,但这并不意味着在R之后的读取会观察到在W之前所发生的写入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var a, b int

func f() {
a = 1
b = 2
}

func g() {
print(b)
print(a)
}

func main() {
go f()
g()
}

这里可能发生的情况是g()打印出了2和0,也就是即便g()已经读取了f()里面对b的写入,但这不意味着g()里面的a能够读取到f()中在b写入之前的a。

同理,类似的错误也会发生在同步的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var a string
var done bool

func setup() {
a = "hello, world"
done = true
}

func doprint() {
if !done {
once.Do(setup)
}
print(a)
}

func twoprint() {
go doprint()
go doprint()
}

这里并不意味着能够观察到done设置为true,就隐式地说明a已经被初始化。

另一种典型错误则是忙等,这种情况下并不意味着done被设置为true后,能够表示a已经被初始化,可以跳出for循环。真实情况是,此时print(a),a可能还是空字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var a string
var done bool

func setup() {
a = "hello, world"
done = true
}

func main() {
go setup()
for !done {
}
print(a)
}

应对这些问题也很简单,使用显式地同步。