Golang内存模型
简介
Golang 内存模型指定了一种条件,在这种条件下,一个goroutine 中变量的读取可以保证观察到不同goroutine 中相同变量的写入所产生的值。官方文档建议修改由多个 goroutine 同时访问的数据的程序必须序列化这种访问。要序列化访问,请使用 channel 操作或其他同步原语(sync 和sync/atomic )保护数据。如果您必须阅读本文档的其余部分才能理解程序的行为,那么您太聪明了。 别自作聪明。 happens before
在一个 goroutine 中,读写操作必须按照程序指定的顺序执行。也就是说,只有当重新排序不会改变语言规范定义的某个goroutine 中的行为时,编译器和处理器才可以对单个goroutine 中执行的读写指令进行重新排序。由于存在重新排序,一个goroutine 观察到的执行顺序可能与另一个goroutine 观察到的执行顺序不同。例如,如果一个goroutine 执行a=1;b=2; ,另一个可能会在a 的更新值之前观察到b 的更新值。
为了确定读写的需求, Golang 定义了程序中执行内存操作的偏序:如果事件e1 发生在事件e2 之前,那么我们说e2 发生在事件e1 之后,同样,如果e1 不发生在e2 之前,也不发生在e2 之后,那么我们说e1 和e2 同时发生。
在一个 goroutine 内,happens-before 顺序就是程序编写的顺序。如果以下两种情况均成立,则允许变量 v 的读取r 观察到对v 的写入 w :r does not happen before w.
There is no other write w to v that happens after w but before r.为了保证变量 v 的读 r 观察到特定的写 w ,确保 w 是唯一被允许观察的写。也就是说,如果以下两个条件均成立,则 r 保证观察到 w :w happens before r.
Any other write to the shared variable v either happens before w or after r.
这一对条件比第一对条件强;它要求没有其他写操作与 w 或 r 同时发生。
在单个 goroutine 中,没有并发性,因此这两个定义是等效的:read r 观察由最近的write w 写入 v 的值。
当多个 goroutine 访问共享变量 v 时,它们必须使用同步事件来建立在确保读取观察所需写入之前发生的条件。
变量的初始化在内存模型中被认为是write。
大于一个机器字的值的读写行为与多个单机器字大小的操作一样,无顺序。 同步初始化
程序的初始化在一个特定的 goroutine 中完成,但是该协程可以创建其他协程并发运行。
如果包 p 引入了包q ,则包q 的init 的函数执行完成happens before 包p 的init 函数开始执行。
main.main 函数的执行happens after 所有init 函数的执行完成。协程创建
开始新 goroutine 的go 语句发生在goroutine 执行开始之前。var a string func f() { print(a) } func hello() { a = "hello, world" go f() }
上面的代码输出 hello world . 因为a 的写(第8行)happen before 协程创建(第9行),所以a 的写happen before 协程的执行。协程销毁
goroutine 的退出不能保证发生在程序中的任何事件之前。var a string func hello() { go func() { a = "hello" }() print(a) }
上面的代码并不能保证一定输出 hello 。因为分配给a 之后没有任何同步事件,因此不能保证任何其他goroutine 都能观察到它。一个激进的编译器可能会删除整个go 语句。
如果一个 goroutine 的效果必须由另一个goroutine 观察,则应该使用同步机制(如锁或通道通信)来建立相对顺序。通道(channel)通信
通道通信是 goroutine 之间同步的主要方法。特定通道上的每个发送都与该通道的相应接收相匹配。
有缓冲通道上的发送发生在该通道相应的接收完成之前。 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 ,因为a 的写操作发生在通道写之前,通道写发生在读之前。
通道关闭发生在因为通道已关闭返回零值的接收之前。 var c = make(chan int, 10) var a string func f() { a = "hello, world" close(c) } func main() { go f() <-c print(a) }
这个代码和上面代码有相同的效果。
从无缓冲通道的接收发生在该通道上的发送完成之前。 var c = make(chan int) var a string func f() { a = "hello, world" <-c } func main() { go f() c <- 0 print(a) }
上述代码可以打印 hello, world , 对a 的写入发生在c 上的接收之前,发生在c 上相应的发送完成之前,发生在打印之前。var c = make(chan int, 1) var a string func f() { a = "hello, world" <-c } func main() { go f() c <- 0 print(a) }
上述代码不能保证输出 hello world ,可能会造成程序崩溃或者打印空字符串。
容量为 C 的通道上的第k 次接收发生在该通道的第k+C 次发送完成之前。
此规则将上一个规则推广到缓冲通道。它允许通过缓冲通道对计数信号量进行建模:通道中的元素数量对应于活动使用的数量,通道容量对应于同时使用的最大数量,发送元素获取信号量,接收元素释放信号量。这是限制并发的常用习惯用法。 var limit = make(chan int, 3) func main() { for _, w := range work { go func(w func()) { limit <- 1 w() <-limit }(w) } select{} }
上面代码用有 buffer 的管道限制同时工作的协程的最大数量。锁
sync 包实现了sync.Mutex 和sync.RWMutex 两种锁
任给 sync.Mutex 或sync.RWMutex 的实例l 和n