Go 内存模型定义了一个 goroutine 中的变量读操作可以观察到另一个 goroutine 中对同一变量写操作产生的值的条件。本文档解释了 Go 内存模型的正式定义、同步机制以及编写并发 Go 程序的实践指南。
Go 通过 goroutines 和 channels 提供了强大的并发特性,但随之而来的是理解在并发上下文中内存操作如何行为的需求。Go 内存模型保证了在一个 goroutine 中所做的更改在另一个 goroutine 中何时可见。
Go 内存模型文档中最重要的一条建议很简单:
修改被多个 goroutine 同时访问的数据的程序必须串行化此类访问。
为了串行化访问,请使用 channel 操作或其他同步原语(如 `
sync` 和 `
sync/atomic` 包中的原语)来保护数据。
在没有数据竞争的情况下,Go 程序行为就如同所有 goroutine 都被多路复用到单个处理器上一样。这个属性被称为“DRF-SC”(无数据竞争的顺序一致性)。
来源:doc/go_mem.html13-42 doc/go_mem.html44-63
Go 的内存模型通过几个关键概念来定义:
内存操作由四个方面表征:
有些操作是读操作(读取数据、互斥锁、channel 接收),而另一些是写操作(写入数据、解锁互斥锁、发送 channel 消息、关闭 channel)。
正式定义建立了几个关系:
顺序发生 (Sequenced before):单个 goroutine 内的操作顺序,由 Go 的控制流定义。
同步发生 (Synchronized before):同步操作上的部分排序。如果同步读操作观察到了同步写操作,则写操作发生在读操作之前。
发生 (Happens before):顺序发生和同步发生关系的联合传递闭包。
当来自不同 goroutine 的两个操作访问同一内存位置,其中至少一个是写操作,并且它们没有被“发生”关系排序时,就会发生数据竞争。
数据竞争可以分为:
如果程序没有数据竞争,那么它的行为将是顺序一致的,就如同来自不同 goroutine 的操作以某种一致的总顺序交错执行一样。
Go 的内存模型对实现遇到数据竞争程序时可以做什么施加了限制:
任何实现都可以检测到数据竞争并停止程序执行。运行带有 `-
race` 标志的程序时就会发生这种情况。
对于单字或子字内存位置,读操作必须观察到来自先前发生或并发发生的写操作的值。
对于多字读/写,不能保证观察到来自单个写操作的一致值。
这些限制可以防止数据竞争的一些最坏后果,使 Go 程序比 C 或 C++ 等语言更具可预测性,在这些语言中,数据竞争会导致未定义行为。
Go 提供了几种建立不同 goroutine 之间操作的发生关系(happens-before relationships)的机制。
包初始化按定义好的顺序执行
p` 导入了包 `
q`,则 `
q` 的 `
init` 函数在 `
p` 的任何函数开始之前完成。
init` 函数都在 `
main.main` 开始之前完成。
启动新 goroutine 的 `
go` 语句在 goroutine 执行开始之前是同步的。这确保了在 `
go` 语句之前初始化的变量对新 goroutine 是可见的。
Channels 是 goroutine 之间进行同步的主要方式。
`
sync` 包中的 Mutex 和 RWMutex 提供了同步机制。
l`,第 n 次调用 `
l.Unlock()` 在第 m 次调用 `
l.Lock()` 返回之前是同步的,其中 n < m。
`
sync.Once` 类型确保函数即使在多个 goroutine 的存在下也只执行一次。
once.Do(f)` 的完成与任何调用 `
once.Do(f)` 的返回是同步的。
`
sync/atomic` 包中的操作建立发生关系(happens-before relationships)。
此代码不正确,因为将 `
done` 视为 true 并不能保证对 `
a` 的写入是可见的。
此代码不正确,原因与前一个示例相同。此外,编译器可能会将忙等待循环优化成一个无限循环。
这两种情况的正确方法是使用适当的同步原语。
Go 的内存模型也限制了哪些编译器优化是有效的。编译器不得
这些限制确保了带有数据竞争的 Go 程序的行为,虽然不是完全定义的,但比 C/C++ 等语言更具可预测性。
以下是一些编写正确并发 Go 程序的实践指南:
sync` 和 `
sync/atomic` 包中的同步原语。
go build -race`、`
go test -race`)来识别数据竞争。
如有疑问,请遵循 Go 内存模型文档的建议:
如果你必须阅读本文档的其余部分来理解程序的行为,说明你太聪明了。
不要耍小聪明。
Go 的内存模型提供了一套清晰的规则,用于理解一个 goroutine 中的内存操作何时对另一个 goroutine 可见。通过遵循正确的同步实践并避免数据竞争,您可以编写出正确且高效的并发程序。
在没有数据竞争的情况下,Go 提供顺序一致性,使程序更容易推理。当存在数据竞争时,Go 提供的保证比 C/C++ 多,但比完全顺序一致的模型少。
请记住,竞速检测器是识别程序中数据竞争的宝贵工具,但它无法检测到所有竞速。最好的方法是从一开始就通过使用适当的同步机制来设计程序,使其避免数据竞争。