head

介绍

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:

  1. r没有先发生于w
  2. 没有其他对v的写w’ ,后发生于w, 先发生于r (也就是在w 和 r之间不存在 w')

为了保证r能读到w的写,要确保w是允许r观察的唯一写入.也就是说,如果以下两个条件均成立,则保证r观察到w:

  1. w先发生于r
  2. 任何其他写入共享变量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.Mutexsync.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不一样,这时候我们需要用同步原语,串行化,让程序形成我们需要的顺序