这是我参与2022首次更文挑战的第10天,活动详情查看:2022首次更文挑战
原文链接:golang.org/ref/mem
介绍
go内存模型指定了在一个goroutine中对一个变量的读取,可以保证观察到被另一个不同的goroutine写到同一个变量的值。
建议
程序修改数据被多个goroutine同时访问必须保证必须序列化这样的访问。
为了序列化访问,用管道操作或者其他同步原语来保护数据,比如那些在sync和sync/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,如果满足以下两个条件:
- r没有happen before w。
- 没有其他对v的写w’ happens after w但是happen before r。
为了保证变量v的读r观察到对v特别的写w,确认w是唯一的写能让r被允许观察到。那就是,r被保证观察到w如果以下两个条件都持有:
- w happens before r
- 任何其他对变量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(管道通信)》,敬请期待。