1 簡介
Go 內(nèi)存模型指定了一個條件,在該條件下,在一個 goroutine 中一個變量的讀取可保證能夠觀測到被其他 goroutine 對該變量寫入的變化值。
2 建議
修改能夠被多個 goroutine 同時訪問到的數(shù)據(jù)的程序必須序列化此過程。
為了序列化這個訪問過程,使用通道操作或其他例如在 sync 和 sync/atomic 包中的同步原語保護數(shù)據(jù)。
如果你一定要閱讀該文檔的剩余部分以理解程序的行為,那么你就太聰明了。
不要自作聰明。
3 先行發(fā)生原則(Happens Before)
在一個 goroutine 中,讀寫一定會以在程序中的指定順序而執(zhí)行。這意味著,編譯器和處理器可能會重新排序在一個 goroutine 中的讀寫執(zhí)行,但只有當重排序不會改變語言規(guī)范定義的單一 goroutine 內(nèi)的行為表現(xiàn)時才會發(fā)生。由于這種重排序的發(fā)生,一個 goroutine 中觀測到的執(zhí)行順序可能不同于另一個 goroutine 中的觀察。例如,如果一個 goroutine 執(zhí)行 a = 1; b=2;, 另一個可能會觀測到 b 先更新而 a 后更新。
為了滿足讀寫需求,我們定義了 happens before 原則, 這是一種在 Go 程序中描述執(zhí)行內(nèi)存操作的偏序關(guān)系。如果事件 e1 先行發(fā)生于事件 e2, 那么我們說 e2 后發(fā)生于 e1。同理,如果 e1 沒有(一定要)先行發(fā)生于 e2, 那么我們說e1 和 e2 同步發(fā)生。
在一個 goroutine 內(nèi),happens-before 順序由程序表述。
對變量 v 的讀操作 r 被允許觀測到對 v 的寫操作 w 當以下條件同時滿足時:
-
r沒有先行發(fā)生于w。 - 沒有有另一個對
v的 寫操作w'在w之后,r之前發(fā)生。
為了保證 對變量 v 的讀操作 r 能夠觀測到某個對 v 的寫操作 w,要確保 w 是 r 被允許觀測到的唯一的寫操作。這就是說,確保 r 觀測到 w 當同時滿足下列條件:
-
w先行發(fā)生于r。 - 任何其他對共享變量
v的寫操作要么在w之前發(fā)生,要么在r之后發(fā)生。
這對條件的要求要強于第一對條件;它約束了沒有其他的寫操作和 w 或 r 同時發(fā)生。
在一個 goroutine 內(nèi),沒有并發(fā),因此兩個定義是等價的:讀操作 r 觀測到的值是最近的對 v 的寫操作 w 寫入的。當多個 goroutine 同時訪問一個共享變量 v 時,他們必須使用同步事件建立先行發(fā)生(happens-before)條件確保讀取期望的寫入值。
在內(nèi)存模型中對變量 v 的初始化含類型零值的操作其表現(xiàn)與寫操作一致。
讀取和寫入超過一個機器字的值其表現(xiàn)與以非指定順序進行多個機器字操作一致。
4 同步
4.1 初始化
程序初始化運行在一個 goroutine 內(nèi),但是這個 goroutine 可能創(chuàng)建其他 goroutines,客觀產(chǎn)生并發(fā)運行的效果。
如果包 p 引入了包 q, 對 q 的 init 函數(shù)的完成要先行發(fā)生于任何對 p 的函數(shù)的開始。
函數(shù) main.main 的開始要在所有 init 函數(shù)的完成后發(fā)生。
4.2 Goroutine 創(chuàng)建
go 語句啟動了一個新的 goroutine, 先行發(fā)生于 goroutine 的開始執(zhí)行。
例如,對這個程序:
var a string
func f() {
print(a)
}
func hello() {
a = "hello, world"
go f()
}
調(diào)用 hello 將打印 hello, world 在未來某個時點(或許在 hello 函數(shù)返回之后)
4.3 Goroutine 銷毀
不保證一個 goroutine 的退出先行發(fā)生于程序的任何事件。例如,對這個程序:
var a string
func hello() {
go func() { a = "hello" }()
print(a)
}
對 a 的賦值操作并為被任何同步事件所保證,因此不保證任何其他的 goroutine 能夠觀測到該賦值。事實上,一個激進的編譯器或許或刪除整個 go 語句。
如果一個 goroutine 的影響必須被另一個 goroutine 觀測到,就得使用例如一個鎖或通道通信的同步機制建立相對順序。
4.4 通道通信
通道通信是 goroutines 間同步的主要方法。每個特定通道上的發(fā)送操作要與該通道上的接收操作對應(yīng),通常用于不同的 goroutine。
一個通道上的發(fā)送操作在該通道上的接收操作完成之前發(fā)生。
這個程序:
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 的寫操作先行發(fā)生于在通道 c 上的發(fā)送操作,在 c 上的發(fā)送在相關(guān)的接收操作完成之前發(fā)生,在 c 上的接收完成先行發(fā)生于 print 函數(shù)。
一個通道上的關(guān)閉操作在由于通道被關(guān)閉的原因接收到返回的零值之前發(fā)生。
在前面的例子里,用 close(c) 代替 c <- 0 會使程序表現(xiàn)同樣的行為。
一個非緩沖通道上的接收操作在該通道上的發(fā)送操作完成之前發(fā)生。
以下程序(如同上面的程序,但是交換了發(fā)送和接收語句,使用了非緩沖通道):
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 的接收操作之前發(fā)生,對通道 c 的接收操作在相關(guān)的發(fā)送操作完成之前發(fā)生,對通道 c 的發(fā)送完成在 print 函數(shù)之前發(fā)生。
如果通道是緩沖的(例如,c = make(chan int, 1)),那么程序?qū)⒉荒鼙WC打印出 hello, world。(可能會打印出空字符串,崩潰或產(chǎn)生其他什么效果。)
容量為 C 的通道上第 k 個接收操作在該通道第 k + C 個發(fā)送操作完成之前發(fā)生。
這個規(guī)則泛化了之前對于帶緩沖通道的規(guī)則。它允許計數(shù)信號量由帶緩沖通道建模: 通道中的條目數(shù)對應(yīng)于活躍使用數(shù),通道容量對應(yīng)于最大的同時使用數(shù),發(fā)送一個條目獲取信號量,接收一個條目釋放信號量。這是限制并發(fā)的常用習慣用法。
以下程序在工作列表中每次進入都會啟動一個 goroutine, 但是 goroutines 使用 limit 通道進行協(xié)調(diào)以確保至多只有3個工作函數(shù)同時運行。
var limit = make(chan int, 3)
func main() {
for _, w := range work {
go func(w func()) {
limit <- 1
w()
<-limit
}(w)
}
select{}
}
4.5 鎖
sync 包引入了兩種鎖類型, sync.Mutex 和 sync.RWMutex。
對于任何 sync.Mutex 或 sync.RWMutex 變量 l 和 n < m,對 l.Unlock() 的調(diào)用 n 在對 l.Lock() 的調(diào)用 m 返回之前發(fā)生。
以下程序:
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() 的第一次調(diào)用 (在 f 中) 在對 l.Lock() 的第二次調(diào)用(在 main 中)返回之前發(fā)生,而對 l.Lock() 的第二次調(diào)用在 print 函數(shù)之前發(fā)生。
對于任何對 sync.RWMutext 變量 l 的 l.RLock() 調(diào)用,存在一個這樣的調(diào)用 n, 其 l.RLock 在 l.Unlock 的調(diào)用 n 之后發(fā)生(返回),匹配的 l.RUnlock 在 l.Lock 的調(diào)用 n + 1 前發(fā)生。
4.6 Once
sync 包提供了一種機制,該機制允許多個 goroutines 使用 Once 類型進行安全的初始化。多個線程對一個特定的函數(shù) f 能都執(zhí)行 once.Do(f), 但是只有一個會運行 f(), 其他調(diào)用會阻塞直到 f() 返回。
通過 once.Do(f) 對 f() 的調(diào)用在任何調(diào)用 once.Do(f) 返回之前發(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()
}
調(diào)用 twoprint 會造成 hello, world 被打印兩次。第一次對 doprint 的調(diào)用會運行 setup 一次。
5 不正確的同步
注意到讀操作 r 可能觀測到和 r 同時(并發(fā))發(fā)生的寫操作 w 寫入的值。即使這種情況發(fā)生,但并不意味著任何發(fā)生在 r 之后的讀操作能夠觀測到 w 之前的寫操作寫入的值。
在以下程序中:
var a, b int
func f() {
a = 1
b = 2
}
func g() {
print(b)
print(a)
}
func main() {
go f()
g()
}
有可能 g 打印出 2 和 0。
這個事實會使一些常見用法無效。
雙重檢查鎖是一種避免同步開銷的方法。例如,twoprint 程序可能會被不正確地寫為:
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 函數(shù)中,觀測到 done 的寫入意味著觀測到 a 的寫入值。這個版本可能會(不正確地)打印出一個空字符串,而不是 hello, world。
另一個不正確的習慣用法是忙于等待一個值,例如:
var a string
var done bool
func setup() {
a = "hello, world"
done = true
}
func main() {
go setup()
for !done {
}
print(a)
}
正如之前的程序,無法保證在 main 中,觀測到對 done 的寫入意味著觀測到對 a 的寫入。因此,這個程序也可能打印出空字符串。更糟糕的事,無法保證對 done 的寫入會被 main 函數(shù)觀測到,因為在兩個線程之間沒有同步機制。在 main 中的循環(huán)不保證能夠結(jié)束。
又一個這種模式的微妙變體,例如如下程序:
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)
}
即使 main 函數(shù)觀測到 g != nil 并且退出了循環(huán),仍然無法保證它能觀測到 g.msg 的初始化值。
在上述所有的例子中,解決方案只有一個:使用顯式同步機制。