1 簡(jiǎn)介
Go 內(nèi)存模型指定了一個(gè)條件,在該條件下奕坟,在一個(gè) goroutine 中一個(gè)變量的讀取可保證能夠觀測(cè)到被其他 goroutine 對(duì)該變量寫入的變化值。
2 建議
修改能夠被多個(gè) goroutine 同時(shí)訪問(wèn)到的數(shù)據(jù)的程序必須序列化此過(guò)程。
為了序列化這個(gè)訪問(wèn)過(guò)程,使用通道操作或其他例如在 sync
和 sync/atomic
包中的同步原語(yǔ)保護(hù)數(shù)據(jù)骏掀。
如果你一定要閱讀該文檔的剩余部分以理解程序的行為,那么你就太聰明了。
不要自作聰明截驮。
3 先行發(fā)生原則(Happens Before)
在一個(gè) goroutine 中笑陈,讀寫一定會(huì)以在程序中的指定順序而執(zhí)行。這意味著葵袭,編譯器和處理器可能會(huì)重新排序在一個(gè) goroutine 中的讀寫執(zhí)行涵妥,但只有當(dāng)重排序不會(huì)改變語(yǔ)言規(guī)范定義的單一 goroutine 內(nèi)的行為表現(xiàn)時(shí)才會(huì)發(fā)生。由于這種重排序的發(fā)生坡锡,一個(gè) goroutine 中觀測(cè)到的執(zhí)行順序可能不同于另一個(gè) goroutine 中的觀察蓬网。例如,如果一個(gè) goroutine 執(zhí)行 a = 1; b=2;
, 另一個(gè)可能會(huì)觀測(cè)到 b
先更新而 a
后更新鹉勒。
為了滿足讀寫需求帆锋,我們定義了 happens before
原則, 這是一種在 Go 程序中描述執(zhí)行內(nèi)存操作的偏序關(guān)系贸弥。如果事件 e1
先行發(fā)生于事件 e2
, 那么我們說(shuō) e2
后發(fā)生于 e1
窟坐。同理海渊,如果 e1
沒(méi)有(一定要)先行發(fā)生于 e2
, 那么我們說(shuō)e1
和 e2
同步發(fā)生绵疲。
在一個(gè) goroutine 內(nèi),happens-before 順序由程序表述臣疑。
對(duì)變量 v
的讀操作 r
被允許觀測(cè)到對(duì) v
的寫操作 w
當(dāng)以下條件同時(shí)滿足時(shí):
-
r
沒(méi)有先行發(fā)生于w
盔憨。 - 沒(méi)有有另一個(gè)對(duì)
v
的 寫操作w'
在w
之后,r
之前發(fā)生讯沈。
為了保證 對(duì)變量 v
的讀操作 r
能夠觀測(cè)到某個(gè)對(duì) v
的寫操作 w
郁岩,要確保 w
是 r
被允許觀測(cè)到的唯一的寫操作。這就是說(shuō)缺狠,確保 r
觀測(cè)到 w
當(dāng)同時(shí)滿足下列條件:
-
w
先行發(fā)生于r
问慎。 - 任何其他對(duì)共享變量
v
的寫操作要么在w
之前發(fā)生,要么在r
之后發(fā)生挤茄。
這對(duì)條件的要求要強(qiáng)于第一對(duì)條件如叼;它約束了沒(méi)有其他的寫操作和 w
或 r
同時(shí)發(fā)生。
在一個(gè) goroutine 內(nèi)穷劈,沒(méi)有并發(fā)笼恰,因此兩個(gè)定義是等價(jià)的:讀操作 r
觀測(cè)到的值是最近的對(duì) v
的寫操作 w
寫入的。當(dāng)多個(gè) goroutine 同時(shí)訪問(wèn)一個(gè)共享變量 v
時(shí)歇终,他們必須使用同步事件建立先行發(fā)生(happens-before)條件確保讀取期望的寫入值社证。
在內(nèi)存模型中對(duì)變量 v
的初始化含類型零值的操作其表現(xiàn)與寫操作一致。
讀取和寫入超過(guò)一個(gè)機(jī)器字的值其表現(xiàn)與以非指定順序進(jìn)行多個(gè)機(jī)器字操作一致评凝。
4 同步
4.1 初始化
程序初始化運(yùn)行在一個(gè) goroutine 內(nèi)追葡,但是這個(gè) goroutine 可能創(chuàng)建其他 goroutines,客觀產(chǎn)生并發(fā)運(yùn)行的效果。
如果包 p 引入了包 q, 對(duì) q 的 init 函數(shù)的完成要先行發(fā)生于任何對(duì) p 的函數(shù)的開(kāi)始宜肉。
函數(shù) main.main 的開(kāi)始要在所有 init 函數(shù)的完成后發(fā)生疾渣。
4.2 Goroutine 創(chuàng)建
go
語(yǔ)句啟動(dòng)了一個(gè)新的 goroutine, 先行發(fā)生于 goroutine 的開(kāi)始執(zhí)行崖飘。
例如榴捡,對(duì)這個(gè)程序:
var a string
func f() {
print(a)
}
func hello() {
a = "hello, world"
go f()
}
調(diào)用 hello
將打印 hello, world
在未來(lái)某個(gè)時(shí)點(diǎn)(或許在 hello
函數(shù)返回之后)
4.3 Goroutine 銷毀
不保證一個(gè) goroutine 的退出先行發(fā)生于程序的任何事件。例如朱浴,對(duì)這個(gè)程序:
var a string
func hello() {
go func() { a = "hello" }()
print(a)
}
對(duì) a
的賦值操作并為被任何同步事件所保證吊圾,因此不保證任何其他的 goroutine 能夠觀測(cè)到該賦值。事實(shí)上翰蠢,一個(gè)激進(jìn)的編譯器或許或刪除整個(gè) go
語(yǔ)句项乒。
如果一個(gè) goroutine 的影響必須被另一個(gè) goroutine 觀測(cè)到,就得使用例如一個(gè)鎖或通道通信的同步機(jī)制建立相對(duì)順序梁沧。
4.4 通道通信
通道通信是 goroutines 間同步的主要方法檀何。每個(gè)特定通道上的發(fā)送操作要與該通道上的接收操作對(duì)應(yīng),通常用于不同的 goroutine廷支。
一個(gè)通道上的發(fā)送操作在該通道上的接收操作完成之前發(fā)生频鉴。
這個(gè)程序:
var c = make(chan int, 10)
var a string
func f() {
a = "hello, world"
c <- 0
}
func main() {
go f()
<-c
print(a)
}
這個(gè)程序能保證打印出 hello, world
。對(duì) a
的寫操作先行發(fā)生于在通道 c
上的發(fā)送操作恋拍,在 c
上的發(fā)送在相關(guān)的接收操作完成之前發(fā)生垛孔,在 c
上的接收完成先行發(fā)生于 print
函數(shù)。
一個(gè)通道上的關(guān)閉操作在由于通道被關(guān)閉的原因接收到返回的零值之前發(fā)生施敢。
在前面的例子里周荐,用 close(c)
代替 c <- 0
會(huì)使程序表現(xiàn)同樣的行為。
一個(gè)非緩沖通道上的接收操作在該通道上的發(fā)送操作完成之前發(fā)生僵娃。
以下程序(如同上面的程序概作,但是交換了發(fā)送和接收語(yǔ)句蘸嘶,使用了非緩沖通道):
var c = make(chan int)
var a string
func f() {
a = "hello, world"
<-c
}
func main() {
go f()
c <- 0
print(a)
}
該程序也能保證打印出 hello, world
象颖。對(duì) a
的寫操作在對(duì)通道 c
的接收操作之前發(fā)生,對(duì)通道 c
的接收操作在相關(guān)的發(fā)送操作完成之前發(fā)生练链,對(duì)通道 c
的發(fā)送完成在 print
函數(shù)之前發(fā)生先壕。
如果通道是緩沖的(例如瘩扼,c = make(chan int, 1)
),那么程序?qū)⒉荒鼙WC打印出 hello, world
垃僚。(可能會(huì)打印出空字符串集绰,崩潰或產(chǎn)生其他什么效果。)
容量為 C 的通道上第 k 個(gè)接收操作在該通道第 k + C 個(gè)發(fā)送操作完成之前發(fā)生谆棺。
這個(gè)規(guī)則泛化了之前對(duì)于帶緩沖通道的規(guī)則栽燕。它允許計(jì)數(shù)信號(hào)量由帶緩沖通道建模: 通道中的條目數(shù)對(duì)應(yīng)于活躍使用數(shù)罕袋,通道容量對(duì)應(yīng)于最大的同時(shí)使用數(shù),發(fā)送一個(gè)條目獲取信號(hào)量碍岔,接收一個(gè)條目釋放信號(hào)量浴讯。這是限制并發(fā)的常用習(xí)慣用法。
以下程序在工作列表中每次進(jìn)入都會(huì)啟動(dòng)一個(gè) goroutine, 但是 goroutines 使用 limit
通道進(jìn)行協(xié)調(diào)以確保至多只有3個(gè)工作函數(shù)同時(shí)運(yùn)行蔼啦。
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
榆纽。
對(duì)于任何 sync.Mutex 或 sync.RWMutex 變量 l 和 n < m,對(duì) l.Unlock() 的調(diào)用 n 在對(duì) 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
奈籽。對(duì) l.Unlock()
的第一次調(diào)用 (在 f
中) 在對(duì) l.Lock()
的第二次調(diào)用(在 main
中)返回之前發(fā)生,而對(duì) l.Lock()
的第二次調(diào)用在 print
函數(shù)之前發(fā)生鸵赫。
對(duì)于任何對(duì) sync.RWMutext
變量 l
的 l.RLock()
調(diào)用衣屏,存在一個(gè)這樣的調(diào)用 n
, 其 l.RLock
在 l.Unlock
的調(diào)用 n
之后發(fā)生(返回),匹配的 l.RUnlock
在 l.Lock
的調(diào)用 n + 1
前發(fā)生辩棒。
4.6 Once
sync
包提供了一種機(jī)制狼忱,該機(jī)制允許多個(gè) goroutines 使用 Once
類型進(jìn)行安全的初始化。多個(gè)線程對(duì)一個(gè)特定的函數(shù) f
能都執(zhí)行 once.Do(f)
一睁, 但是只有一個(gè)會(huì)運(yùn)行 f()
, 其他調(diào)用會(huì)阻塞直到 f()
返回钻弄。
通過(guò) once.Do(f) 對(duì) 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
會(huì)造成 hello, world
被打印兩次卖局。第一次對(duì) doprint
的調(diào)用會(huì)運(yùn)行 setup
一次斧蜕。
5 不正確的同步
注意到讀操作 r
可能觀測(cè)到和 r
同時(shí)(并發(fā))發(fā)生的寫操作 w
寫入的值。即使這種情況發(fā)生砚偶,但并不意味著任何發(fā)生在 r
之后的讀操作能夠觀測(cè)到 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洒闸。
這個(gè)事實(shí)會(huì)使一些常見(jiàn)用法無(wú)效染坯。
雙重檢查鎖是一種避免同步開(kāi)銷的方法。例如丘逸,twoprint
程序可能會(huì)被不正確地寫為:
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ù)中单鹿,觀測(cè)到 done
的寫入意味著觀測(cè)到 a
的寫入值。這個(gè)版本可能會(huì)(不正確地)打印出一個(gè)空字符串深纲,而不是 hello, world
仲锄。
另一個(gè)不正確的習(xí)慣用法是忙于等待一個(gè)值,例如:
var a string
var done bool
func setup() {
a = "hello, world"
done = true
}
func main() {
go setup()
for !done {
}
print(a)
}
正如之前的程序湃鹊,無(wú)法保證在 main
中儒喊,觀測(cè)到對(duì) done
的寫入意味著觀測(cè)到對(duì) a
的寫入。因此币呵,這個(gè)程序也可能打印出空字符串怀愧。更糟糕的事,無(wú)法保證對(duì) done
的寫入會(huì)被 main
函數(shù)觀測(cè)到,因?yàn)樵趦蓚€(gè)線程之間沒(méi)有同步機(jī)制芯义。在 main
中的循環(huán)不保證能夠結(jié)束哈垢。
又一個(gè)這種模式的微妙變體,例如如下程序:
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ù)觀測(cè)到 g != nil
并且退出了循環(huán)扛拨,仍然無(wú)法保證它能觀測(cè)到 g.msg
的初始化值耘分。
在上述所有的例子中,解決方案只有一個(gè):使用顯式同步機(jī)制绑警。