Introduction
Go内存模型说明在什么情况下,可以保证一个goroutine中某变量a的读取,可以是另外一个goroutine对变量a写入的值。
Advice
修改由多个goroutine同时访问的数据的程序必须序列化此类访问。要序列化访问,请使用channel或其他同步原语(如sync和sync/atomic包中的同步原语)保护数据。
Happens Before
在一个goroutine中,读写操作必须按照程序指定的顺序执行。也就是说,只有在重新排序不会改变语言规范定义的goroutine内的行为时,编译器和处理器才可以重新排序在单个goroutine内执行的读写操作。由于这种重新排序,一个goroutine观察到的执行顺序可能与另一个goroutine观察到的顺序不同。例如,如果一个goroutine执行a=1;b=2;
,另一个goroutine可能会在a的更新值之前观察b的更新值。
为了指定读写的需求,我们定义了happens before(Go程序中内存操作执行的部分顺序)。如果事件e1发生在事件e2之前,那么我们说e2发生在事件e1之后。另外,如果e1不发生在e2之前,也不发生在e2之后,那么我们说e1和e2同时发生。
在单个goroutine中,happers before顺序就是语句表示的顺序。
V = W
...
R = V
在什么情况下R读到的值就是W呢?
情况一
如果同时满足下面的条件,则可能读到:
- 动作读r没有happen before 动作写w
- 没有其他的写w1 happen after w 但是 before r
情况二
如果同时满足下面的条件,则保证读到:
- 动作写 w happen before 动作读 r
- 其他动作写 happens before w or after r
Synchronization
Initialization
程序初始化在单个goroutine中运行,但该goroutine可能会创建其他goroutine,这些goroutine同时运行。
- 如果包p导入包q,q的init函数 happen before p
- 所有的init函数happen before main.main函数
Goroutine creation
从某函数启动一个goroutine,happen before该goroutine执行。
package main
import "time"
var a string
func f() {
print(a)
}
func hello() {
go f()
a = "hello, world" // 放在go前面,输出也是一样的,hello world
}
func main() {
hello()
<-time.After(1)
}
https://play.golang.org/p/th21kxXhRvU
Goroutine destruction
在某个函数里面,某个goroutine的结束不能保证happen before该函数里面的动作。
package main
import "time"
var a string
func hello() {
go func() { a = "hello" }()
print(a)
}
func main() {
hello()
<-time.After(1)
}
修正该问题,需要加上channel或者锁
Channel communication
buffered channel
- A send on a channel happens before the corresponding receive from that channel completes.
- close channel happend before chanel receive
package main
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
https://play.golang.org/p/qbn76DYCceg
unbuffered channel
A receive from an unbuffered channel happens before the send on that channel completes.
package main
var c = make(chan int)
var a string
func f() {
a = "hello, world"
<-c
}
func main() {
go f()
c <- 0
print(a)
}
输出 hello,world
https://play.golang.org/p/M15nzoy6Wlk
但是,上面的代码中,如果是对bufferd channel,输出的结果不可预知。
The kth receive on a channel with capacity C happens before the k+Cth send from that channel completes.
var limit = make(chan int, 3)
func main() {
for _, w := range work {
go func(w func()) {
limit <- 1
w()
<-limit
}(w)
}
select{}
}
Locks
For any sync.Mutex or sync.RWMutex variable l and n < m, call n of l.Unlock() happens before call m of l.Lock() returns.
var l sync.Mutex
var a string
func f() {
a = "hello, world"
l.Unlock()
}
func main() {
l.Lock()
go f()
l.Lock()
print(a)
}
Once
A single call of f() from once.Do(f) happens (returns) before any call of once.Do(f) returns.
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()
}
打印2次
Incorrect synchronization
Note that a read r may observe the value written by a write w that happens concurrently with r. Even if this occurs, it does not imply that reads happening after r will observe writes that happened before w.
package main
var a, b int
func f() {
a = 1
b = 2
}
func g() {
print(b)
print(a)
}
func main() {
go f()
g()
}
输出: 00
https://play.golang.org/p/cPcl12-uJFg
var a string
var done bool
func setup() {
a = "hello, world"
done = true
}
func doprint() {
if !done {
once.Do(setup)
}
print(a)
}
func twoprint() {
go doprint()
go doprint()
}
在doprint中,观察到done的值发生改变,不一定观察到a的值发生改变
var a string
var done bool
func setup() {
a = "hello, world"
done = true
}
func main() {
go setup()
for !done {
}
print(a)
}
和上面一个例子是一样的。还有一个问题就是,这个done可能一直为false
type T struct {
msg string
}
var g *T
func setup() {
t := new(T)
t.msg = "hello, world"
g = t
}
func main() {
go setup()
for g == nil {
}
print(g.msg)
}
即使退出循环,但是也不能保证打印出初始化后g.msg的值。
有疑问加站长微信联系(非本文作者)