go内存模型(一)

133 阅读4分钟

这是我参与2022首次更文挑战的第10天,活动详情查看:2022首次更文挑战

原文链接:golang.org/ref/mem

介绍

go内存模型指定了在一个goroutine中对一个变量的读取,可以保证观察到被另一个不同的goroutine写到同一个变量的值。

建议

程序修改数据被多个goroutine同时访问必须保证必须序列化这样的访问。
为了序列化访问,用管道操作或者其他同步原语来保护数据,比如那些在syncsync/atomic包下的。
如果你必须阅读这篇文档剩余部分来理解你程序的行为,你已经太聪明了。
不要太聪明。

Happens Before

在一个单goroutine中,读取和写入必须表现得好像它们由程序指定按顺序执行。也就是,编译器和解析器只有在重排序不会改变语言声明定义的goroutine的行为时,才可能在一个单goroutine中对读写的执行进行重排序。
因为这种重排序,一个goroutine观察执行的顺序可能和另一个goroutine不同。举个例子,如果一个goroutine执行a=1;b=2;,另一个可能观察到b的值在a的值更新之前。

为了指定读写的需求,我们定义了happens before,在一个go程序中的内存操作的局部执行顺序。如果事件e1 happens before 事件e2,我们就说e2 happens after e1。并且,如果e1没有happens before e2,也不happens after e2,我们就说e1和e2并发发生。

在一个单goroutine中,happens-before 顺序就是程序表达的顺序。

变量v的读r允许观察到v的写w,如果满足以下两个条件:

  1. r没有happen before w。
  2. 没有其他对v的写w’ happens after w但是happen before r。

为了保证变量v的读r观察到对v特别的写w,确认w是唯一的写能让r被允许观察到。那就是,r被保证观察到w如果以下两个条件都持有:

  1. w happens before r
  2. 任何其他对变量v的写都happens before w,或者happens after r。

这对条件比第一对更强大;它要求没有其他写操作同时发生在w或者r上。

在一个单独的goroutine,没有并发,所以两个定义是等效的:一个读r观察到最近对v的写入w操作。当多个goroutine访问一个共享的变量v时,它们必须使用同步时间来简历happen-before条件来确保读观察到期望的写。

为变量v的类型初始化为零值在内存模型中,表现得就像一个写操作。

读和写的值比单个机器码更大,表现得就像在一个未指定顺序的多个机器码长度的操作。

Synchronization

Initialization

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

如果一个package p imports package q,那么q的init函数的完成 happens before于任何p的init函数的开始。

main.main函数的开始,happens after于所有的init函数的完成。

Goroutine creation

go关键字声明的是开启一个新的goroutine happens before于这个goroutine的执行开始。
举个例子,在下面这个程序中:

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

调用hello将会打印"hello,world"在将来的某些瞬间(也许在hello已经返回以后)

Goroutine destruction

一个goroutine的退出不保证happen before于程序中运行的任何事件。例如,在这个程序中

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

对a的声明没有跟随任何的同步事件,所以这无法保证被任何其他goroutine观察到。实际上,一个有进取心的边以及可能会删除这整个go的声明。

如果一个goroutine的影响必须被其他goroutine观察到,使用一个同步机制比如说一个lock锁或者channel通信来建立一个关键的顺序。

下一节将为大家带来《Channel communication(管道通信)》,敬请期待。