介绍
Go内存模型指定了一种条件,在这种条件下,可以保证读取一个goroutine中的变量,以观察不同goroutine中写入同一变量所产生的值。
建议
修改多个goroutine同时访问的数据必须序列化访问
序列化访问保护数据使用channel
操作或者其他同步原语比如sync
或者sync/atomic
包
Happens before
在一个goroutine中,读写必须表现得就像它们按照程序指定的顺序执行一样;也就是说 在一个goroutine中,处理器和编译器可以重排读写的执行顺序,仅当重排后的行为不改变语言的设定.因为重排,一个goroutine观察到的执行顺序可能与另一个goroutine观察到的顺序不同.举个例子,如果一个goroutine执行a = 1; b = 2;
,另一个可能会观察到b在a之前更新.
为了指定读写的需要,我们定义了在Go程序中执行内存操作的偏序(partial order)。如果事件$e_1$先发生于事件$e_2$我们说$e_2$后发生于$e_1$. 同样的如果$e_1$没有先发生于$e_2$并且$e_1$没有后发生于$e_2$那么我们说$e_1 e_2$同时发生.
在单个gorutine里, happens-before
的顺序是程序表示的顺序.
如果下面两个条件成立,则允许变量v的读r 观察到对v的写w:
- r没有先发生于w
- 没有其他对v的写w’ ,后发生于w, 先发生于r (也就是在w 和 r之间不存在 w')
为了保证r能读到w的写,要确保w是允许r观察的唯一写入.也就是说,如果以下两个条件均成立,则保证r观察到w:
- w先发生于r
- 任何其他写入共享变量v都要先发生于w或者后发生于r
这对条件比第一对更严格,这一对要求没有其他的写同时发生于w或r
在单个gorutine中没有并发,这两个定义是等价的:读r观察最近写入w到v的值.
在多个gorutine访问一个共享变量v,必须使用同步事件来建立happens-before
条件来确保读到期望的写.
变量v的类型为零值时,变量v的初始化行为就像在内存模型中写入一样。(初始化等同于写入)
读写超过机器字(machine word)的行为就和以未指定的顺序执行多个机器字大小的操作一样(超过machine word 每个machine word 读取和写入顺序可能不是期望的)
总之, 想要在一个gorutine中读到另一个gorutine的写就要保证, 写在读之前发生,如果我们不加控制,这个写先于读的顺序就很难保证,所以我们需要使用atomic或者和lock机制来保证顺序保证happens-before
同步
初始化
程序初始化在单个goroutine中运行,但该goroutine可能会创建其他并发运行的goroutine。
如果包p导入包q,则q的init函数的完成时间在任何p的开始之前。
函数main的开始。main在所有init函数完成后发生。
Goroutine创建
启动新goroutine的go
语句发生在goroutine开始执行之前。
举例
var a string
func f() {
print(a)
}
func hello() {
a = "hello, world"
go f()
}
调用hello将在将来hello, world,可能hello已经返回
Goroutine销毁
goroutine的退出不能保证先发生于任何事件,例如:
var a string
func hello() {
go func() { a = "hello" }()
print(a)
}
赋值a没有跟着任何同步事件,所以不能保证能被其他gorutine观测到.实际上比较激进的编译器会把整个go语句删除
如果一个gorutine的影响必须被其他gorutine观测到,使用同步机制(比如lock,channel)来建立相对的顺序
Channel 交流
Channel交流是goroutines间同步的主要方法.特定channel上的每个发送与该channel的相应接收相匹配,通常在不同的goroutine中。
channel上的发送 先发生于 该通道的相应接收。
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的赋值先发生于 c的发送, 先发生于对应c接收完成, 先发生于print
channel关闭先发生于接收,将接收到零值,因为管道被关闭了
在前面的例子,使用close(c)
替换c<-0
将有相同的行为
接收无缓冲 先发生于 发送完成
和上面例子比较,使用了无缓冲channel, 交换了发送和接收的位置
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完成,先发生于print
如果是有缓冲的channel就不能保证打印hello, world
(可能打印空字符串,崩溃,或者其他),因为a 先发生于接收c, 但不能保证先发生于发送c完成
容量为C的通道上的第k次接收 先发生于 第k+C次发送
这个规则可以用具体例子来理解,比如 k=1 C=3, 那么第1次接收 先发生于第4次发送,因为容量是3, 第1次的不接受,那么第4次的就放不进来,阻塞住
此规则将之前的规则推广到缓冲通道。channel中的item数对应于active的数量,容量对应于同时使用的最大数量,发送item获取信号量,接收item释放信号量。这是限制并发的常见习惯用法。
例子:此程序为work列表中的每个item启动一个goroutine,但goroutine使用限制通道进行协调,以确保一次最多有3个运行。
var limit = make(chan int, 3)
func main() {
for _, w := range work {
go func(w func()) {
limit <- 1
w()
<-limit
}(w)
}
select{}
}
Locks(锁)
sync
包中实现了两种锁数据类型sync.Mutex
和sync.RWMutex
对任意sync.Mutex
或者 sync.RWMutex
变量 l 并且 n < m , 调用第n次l.Unlock()
先发生于 第m次 l.Lock()
具体例子来理解 n=1 m=2 第一次l.Unlock()
先发生于 第二次l.Lock()
例子:
var l sync.Mutex
var a string
func f() {
a = "hello, world"
l.Unlock()
}
func main() {
l.Lock()
go f()
l.Lock()
print(a)
}
这个程序能确保输出"hello, world",第一次调用l.Unlock()
(在f里)先发生于 第二次l.Lock()
(在main里), 先发生于print
对任意调用l.RLock
(l 是sync.RWMutex类型), 存在n在调用l.RLock
后发生于l.Unlock
,与之匹配的l.RUnlock
先发生于n+1的l.RLock
意思就是说读写是互斥的,在写锁解锁后, 读锁才加, 读锁解锁后,下一个写锁才能加
Once
sync包通过使用Once类型,在存在多个goroutine的情况下提供了一种安全的初始化机制
多个线程可以执行once.Do(f)
对于特定的f,只有一个运行f, 其他的都将阻塞,等待f执行完
once.Do(f)中调用的f() 先发生于 任何调用once.Do(f)函数返回
var a string
var once sync.Once
func setup() {
a = "hello, world"
}
func doprint() {
once.Do(setup)
print(a)
}
func twoprint() {
go doprint()
go doprint()
}
调用twoprint将只调用setup一次, setup将在调用任何一个print之前完成, 结果将打印hello, world
两次.
总结
从单个gorutine的视角来看,程序的行为是顺序执行的, 从多个gorutine来看读写的顺序可能合另一个gorutine不一样,这时候我们需要用同步原语,串行化,让程序形成我们需要的顺序