Go 内存模型
发表于 2022-03
并发场景下最讨厌的是对变量的读写操作隐含着违反直觉的结果。这时候,就需要语言的内存模型了。所有语言的内存模型都在讲着一件事情,什么操作才能让写变量的结果被另一个线程/协程读到。
幸好 Go 一直是一个设计的很“简单”的语言,它的内存模型需要记住的结论很少,我们很容易就写出基于 goroutine 的并发安全程序。
先解释一个经典的名词 happens-before
:如果一个事件的发生在另一个事件之前,那么第一个事件的结果必须影响到第二个事件。happens-before
听上去理应如此,但由于现代处理器的多核、指令重排,编译器优化等等因素,happens-before
不是天然成立的。
接下来看一下 Go 内存模型规定了哪些操作是确定的:
- 如果包
p
导入了包q
,那么包q
的init
函数happens-before
包p
的任何代码。 - 所有包的
init
函数happens-before
main.main
函数。 - 用
go
语句启动新的goroutine
,该语句本身happens-before
启动的这个新goroutine
。 channel
上的发送happens-before
该channel
上的接收。- 关闭
channel
的操作happens-before
在channel
上读到零值(零值意味着channel
关闭)。 - 无缓冲
channel
的接收happens-before
该channel
上的发送完成。 - 容量为 C 的
channel
接收到第 k 个元素happens-before
该channel
第 k+C 个元素发送完成。 - 两个变量 n 和 m,n 出现在 m 的前面,若使用互斥锁
sync.Mutex
或sync.RWMutex
保护,那么变量 n 的l.Unlock
happens-before
变量 m 的l.Lock()
。 - 如果有多个对
once.Do(f)
的调用,那么第一个调用f()
函数happens-before
任何其他once.Do(f)
的调用返回。 sync.Cond
的Broadcast
和Signal
调用happens-before
阻塞中Wait
调用。- 对于
Sync.Map
,Load
,LoadAndDelete
和LoadOrStore
都是读操作。Delete
,LoadAndDelete
和Store
都是写操作。LoadOrStore
如果loaded
返回false
,那也是写操作。Sync.Map
的所有写操作happens-before
读操作。 - 对于
Sync.Pool
,Put
happens-before
Get
,New
happens-before
Get
。 - 对于
Sync.WaitGroup
,Done
happens-before
任何一个阻塞中的Wait
。 sync.atomic
包是一些列原子操作的合集,可用于不同 goroutine 间的同步。如果原子操作 B 的观察到原子操作 A 的结果,那么 Ahappens-before
B。一旦使用了原子操作,那么程序便拥有了一致的执行顺序。