本文已参与掘金创作者训练营第三期「话题写作」赛道,详情查看:掘力计划|创作者训练营第三期正在进行,「写」出个人影响力
这个命题,其实是两个主题:Go
,memory model
memory model
针对什么?- 和
Go
有什么关系
为什么要提出这两个问题?
首先你要知道 memory model
针对什么问题,每个语言都可能会针对这个问题给出自己的做法和规范。
其次,Go
提出了什么规范和建议?
带着这两个问题,才能比较好的理解 Go memory model
这个主题。
为什么需要 memory model
问题的背景是多线程并发。在不同的编程语言,不同的编译器 (重排序),不同的处理器 (CPU缓存),在程序并发运行下,会产生和预期不一样的结果。
而我们的目的是:程序结果正确 → 这个就是 memory model 需要面临的问题,也是需要解决的问题。
指令重排
一句话:代码不会按照你写的顺序来执行
type T struct {
value int
}
var done *T
func factory() {
t := new(T)
t.value = 13
done = t
}
func main() {
go factory()
for done == nil {}
print(done.value)
}
可能出现的情况:
- main goroutine 没有检测到 factory goroutine 对 done 的写操作,g 一直为 nil
- main goroutine 观察到 g 不为 nil,也可能打印出空的 msg ⇒ g 的赋值没有完成,但是至少 g 不是 nil
首先这个是可能的情况,不一定会发生,因为程序运行中各种操作的顺序可能不会得到保证,所以在各种乱序下,程序的结果就迷惑起来了。
以上说的是代码重排序,还有一种:指令并行重排序。
现代的 CPU 都是流水线的,同时在执行的指令有多条 (在执行的不同阶段,这与 CPU 架构有),如果 CPU 认为,你代码中的这些指令不存在相关性,那么它就会选择并行执行。并行执行后,代码的顺序进一步被打乱。
这个时候就有一个概念:happens-before
happens-before,在很多语言都有这个概念
happens-before,描述的是两个时间发生的顺序关系。如果在程序运行过程中,指定操作之间的 happens-before 关系,这样就可以保证程序发生的顺序。
CPU缓存
为了提升程序运行效率,CPU 在从内存取数据之后,会把数据存在缓存中,下次取数据的时候,直接从缓存中拿。
当然,缓存数据最终会写入内存,但是在程序运行的过程中,可能内存中的数据不是最新的,最新数据在缓存中。
在现代 CPU 中,一般有多个核,每个 CPU 核都会有自己的缓存,但是内存只有一块,是共享的。何时从缓存中读取数据,CPU 的优化有个标准,就是“线程级”结果正确,也就是说,当 CPU 认为,对于当前线程(不考虑其他线程的修改),如果内存和缓存中的数据一致,那么会用缓存中的数据。
当然,如果有其他线程对共享的内存做了修改,就会导致 CPU 没有意识到内存已经变化,而仍然取缓存中的数据,导致执行错误。
总结
总结一下目的:
- 在编译器(指令重排)和硬件(不同的CPU架构)的优化下,语言需要一个规范来明确并发环境下一个变量的可见性和顺序
- 给开发人员一个并发保障,并提供一些访问控制 (一些组件和库) → 串行化访问
最终一句 🌹 来说明:如何在多线程并发的情况下,限制编译器优化、指令并行优化,控制线程间的变量传递,使得程序计算结果正确
Go 设计提倡
Don't communicate by sharing memory, share memory by communicating. — go-proverbs.github.io/
这句话如果不知道的,先去把 Go 去看看 (默认大家知道)。
但是这句话不是在 Go 中先提出的,而是 Erlang 这个为通信而生的语言 (rabbitmq 由此语言开发)。阐述了 Go 并发模型:『使用通信来共享内存』。
这个 “通信” 意味什么?
- 通过通信同步不同组件之间的消息 → 解决行为
- 通过通信来共享内存就不会导致线程间的竞争 → 解决竞争
竞争
竞争的来源:并发访问共享内存。按照上面说的 Go 提倡:
将共享变量 → channel ,由于同一时间只能有一个 goroutine 访问 channel (内部也是有一个 mutex) ;对于 channel 而言,producer → channel,对于变量的 ownership (借用一下 rust 的概念) 就转交给 channel,而不同的接收方此时也只有一个可以访问 channel,channel → consumer,接受者获取到该 msg 后就可以根据该消息做一系列操作。
Go 是怎么规定的
说一下整体规则:
- 使用channel操作或其他同步原语(sync/atomic包中的原语)来实现串行化访问
- 多个 goroutines 同时访问的数据,必须串形化访问
既然提到 goroutine,先就来说说这个
goroutines
go func() 执行,一定 happens-before 此 goroutine 内的代码执行。
var a string
func f() {
print(a)
}
func hello() {
a = "hello, world"
go f()
}
func main() {
hello()
}
说明一下整个 happens-before 的执行情况:
a = "hello, world"
go f()
f() → print(a)
所以该程序是可以正常输出:hello, world
。
channel
channel 充当通信的重要角色,在前面的论述中已经提到了。这里说说 channel 的 happens-before 的原则:
- send channel happens before recv channel
- close channel happens before read channel
- unbuffered channel,recv channel happens before send channel
- buffered channe,第 n 个 receive 一定 happens before 第 n+m 个 send 的完成
对于 3 给个例子:
var ch = make(chan int)
var s string
func f() {
s = "send from f()"
<-ch
}
func main() {
go f()
ch <- struct{}{}
print(s)
}
你可以认为,在 ch <- struct{}{}
执行时,s 已经被赋值好,所以后面的 s 是可以输出的。Go memory model