Introduction
G 內存模型指定了一些條件,保證了在一個goroutine中如何讀取一個同時被其他goroutine寫入的變量
Advice
如果程序要修改一個被多個goroutine訪問的變量,此類訪問必須被序列化。
爲了序列了訪問,我們可以採用channel操作或者使用sync
或sync/atomic
包下的同步原語來保護數據。
If you must read the rest of this document to understand the behavior of your program, you are being too clever.
Don't be clever.
如果你必須閱讀這篇文檔才能理解你的程序行爲,那麼說明你的程序是有問題的。作者的意思是希望我們用channel或者同步原語來保證數據的併發讀寫,而不是利用內存模型來推斷程序運行行爲
Happens Before
在一個單獨的goroutine中,讀寫必須按照程序指定的順序執行。但是,編譯器和處理器(CPU)可以對讀寫指令進行重排序,僅當這種重排序對程序的執行結果沒有影響[1]。由於這種重排序,一個 goroutine 觀察到的執行順序可能與另一個 goroutine 感知到的順序不同。例如,如果一個 goroutine 執行a = 1; b = 2;
,另一個 goroutine 可能會在 a 的值更新之前觀察到 b 的更新值。
爲了規定這種讀寫順序,我們定義了Happen Before
(先行發生)原則[2],規定了在Go程序中操作內存的局部順序。如果一個事件e1
發生在事件e2
之前(happen before),那麼我們說e2
發生在e1
之後(happen after)。如果e1
既不happen before也不happen after事件e2
,那麼e1
和e2
是併發發生的(happening concurrently)。
在一個單獨的goroutine中,happen before順序就是程序表達的順序
observe
:當讀操作r讀取到的值剛好是寫操作w
執行的結果,那麼我們說w
被r
觀察(observe)到
在滿足下麪條件下,對於變量v
的讀操作r
,可以observe
到對應的寫操作w
-
r
沒有happen before
w
(happen after
orhappen concurrency
) - 沒有其他的寫操作
w'
happen afterw
但happen before
r
(在w
和r
之間沒有另外的確定的寫操作,可以是happening concurrently
)
爲了保證r
一定能observe到特定的w
,即確保w
是r
唯一可以observe
的結果,也就是r
被確保能oberse
到w
,必須保證以下條件:
-
w
happen before
r
- 任何其他的寫操作,要麼
happen before
w
,要麼happen after
r
(即,在w
和r
之間,沒有任何其他的寫事件,包括happen before 和 happening concurrently)
這兩個條件比上一對嚴格多了,它要求在w
和r
之間沒有另外的寫操作happening concurrently
在一個單獨的goroutine中,沒有併發,所以這兩組條件是相同的:一個讀操作r
必然observe
到最近的寫操作w
。當多個goroutine同時訪問一個共享變量v
,必須使用同步事件建立happen before
條件確保r
能observe
到特定的w
變量初始化爲零值,在內存模型中表現爲寫操作
對於大於單個機器字的值的讀取和寫入操作,表現爲以未指定順序進行的多個機器字大小的操作。 即對於一個大於單個機器字(32位機器爲4byte,64位機器爲8byte)的對象,其讀取和寫入都是多個操作,且是happening concurrently
,其順序是不可預測的。
Synchronization
Initialization
程序的初始化在單個 goroutine 中運行,但該 goroutine 可能會創建其他併發運行的 goroutine。
如果包 p 導入包 q,則 q 的 init 函數在包 p 的任何代碼開始之前完成。
函數 main.main 在所有的 init 函數完成後開始執行。
Goroutine creation
go
語句開始一個新的goroutine happen before
新goroutine開始執行
例如:
var a string
func f() {
print(a)
}
func hello() {
a = "hello, world"
go f()
}
調用hello()
將在未來的某個時刻打印hello world
(也許在hello
return之後)打印出來的一定是hello world,因爲a的賦值操作先行發生於go f()
Goroutine destruction
goroutine的終止並沒有保證happen before
程序的任何事件。 例如:
var a string
func hello() {
go func() { a = "hello" }()
print(a)
}
a的賦值並沒有伴隨任何同步事件,所以它並不能確保被其他goroutineobserve
到。事實上,激進的編譯器可能會刪掉整個go func() { a = "hello" }()
語句,因爲它不一定會生效。
如果一個goroutine的效果必須被其他goroutineobserve
,要使用鎖或者channel通訊這類同步機制建立相對順序。
Channel communication
Channel 通信是不同goroutine的主要同步手段。通常在不同的goroutine中,對應特定channel的每個send操作,都有對應的receive操作
channel的send操作 happen before
對應的`receive操作完成之前
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的send操作c<-0
,c的send操作先行發生於mian中對應的recevie操作<-c
,recevie操作先行發生於print(a)
。
當一個channel關閉後,receive會收到零值
channel的close操作happen before
因爲channel cloase收到零值的receive操作
在上面的例子,把c<-0
替換爲close(c)
,程序的執行順序是一致的。
沒有緩衝區的channel的receive操作happen before
對應的send操作完成
下面的程序跟上一個類似,但是send和receive操作交換了,而且使用了unbuffered 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的賦值先行發生於receive操作<-c
,receive操作先行發生於send操作c<-0
完成,send操作完成先行發生於print操作[3]
如果代碼中的channel是一個帶緩衝區的channel(例如c = make(chan int, 1)
),那麼程序將無法保證打印hello world
(可能會打印空字符串,崩潰或執行其他操作。)
對於容量C的channel,第k個receive操作happend before
第k+C個send操作完成
此規則概括了先前的有緩衝的 channel 的規則。它允許用有緩衝的 channel 建立的計數信號量:channel中items的數量對應於資源當前的使用數量,channel的容量對應於資源同時允許的最大使用數量。send一個item到channel中代表獲取一個信號量,從channel中receive一個item代表釋放一個信號量。這是一個限制併發的通用用法。
下面程序爲work list中的每個entry開啓一個goroutine,但是這些goroutine協調使用limit channel確保最多同時有三個goroutine可以運行work方法。
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.Mutex
和sync.RWMutex
兩種鎖類型。
對於每個sync.Mutex
或者sync.RWMutex
類型的鎖l
,如果n<m,那麼調用第n個l.Unlock()
happend before
調用第m個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()
happen before
第二個l.Lock()
返回。
對於sync.RWMutex
類型的鎖l
的每個l.RLock()
調用,l.RLock()
成功返回happen after
第n次l.Unlock()
,那麼對應的l.RUnlock()
返回 happend before
第n+1次l.Lock()
Once
sync
通過Once類型,提供了一種在多個goroutine下初始化的安全機制。多個goroutine通過once.Do(f)
執行特定的f()
,但只有一個goroutine會真正執行,而其他goroutine會阻塞直到f()
執行結束返回。
通過once.Do(f)執行的唯一f()
返回 happen before
於任意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()
返回happen before
任意一個print
,所以程序會打印兩次hello world。
Incorrect synchronization
注意,讀取操作 r 可以觀察到與r同時發生的寫入操作 w 所寫的值。即使發生這種情況,也不意味着在 r 之後發生的讀取操作將觀察到在 w 之前發生的寫入操作。
var a, b int
func f() {
a = 1
b = 2
}
func g() {
print(b)
print(a)
}
func main() {
go f()
g()
}
在上面的程序中,可能會打印2和0。f()
中的a=1
和b=2
並沒有happen before關係
這個事實使得一些常用習慣性用法失效。
雙重檢查鎖(Double-checked lock)是一種爲了避免同步開銷的用法。例如,上面的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中,觀察到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的寫入操作,所以這個程序也可能打印一個空字符串。更糟糕的是,無法保證main方法可以observe帶done的寫操作,因爲兩個goroutine之間並沒有同步事件,main方法的循環無法保證一定會退出。
這個主題有一些微小的變種,如下:
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能observe到 g!=nil
,也無法保證g.msg被初始化。
在這些例子中,解決方案都是一樣的:使用顯示的同步機制
總結
- Within a single goroutine, the happens-before order is the order expressed by the program.
在一個單獨的goroutine中,happen before順序就是程序表達的順序 - If a package p imports package q, the completion of q's init functions happens before the start of any of p's.
如果包 p 導入包 q,則 q 的 init 函數在包 p 的任何代碼開始之前完成。 - The start of the function main.main happens after all init functions have finished.
函數 main.main 在所有的 init 函數完成後開始執行。 - The go statement that starts a new goroutine happens before the goroutine's execution begins.
go語句開始一個新的goroutine happen before 新goroutine開始執行 - The exit of a goroutine is not guaranteed to happen before any event in the program.
goroutine的終止並沒有保證happen before程序的任何事件。 - A send on a channel happens before the corresponding receive from that channel completes.
channel的send操作 happen before對應的`receive操作完成之前 - The closing of a channel happens before a receive that returns a zero value because the channel is closed.
channel的close操作happen before 因爲channel cloase收到零值的receive操作 - A receive from an unbuffered channel happens before the send on that channel completes.
沒有緩衝區的channel的receive操作happen before對應的send操作完成 - The kth receive on a channel with capacity C happens before the k+Cth send from that channel completes.
對於容量C的channel,第k個receive操作happend before第k+C個send操作完成 - 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.
對於每個sync.Mutex或者sync.RWMutex類型的鎖l,如果n<m,那麼調用第n個l.Unlock() happend before 調用第m個l.Lock()返回 - For any call to l.RLock on a sync.RWMutex variable l, there is an n such that the l.RLock happens (returns) after call n to l.Unlock and the matching l.RUnlock happens before call n+1 to l.Lock.
對於sync.RWMutex類型的鎖l的每個l.RLock()調用,l.RLock()成功返回happen after 第n次l.Unlock(),那麼對應的l.RUnlock()返回 happend before 第n+1次l.Lock() - A single call of f() from once.Do(f) happens (returns) before any call of once.Do(f) returns.
通過once.Do(f)執行的唯一f()返回 happen before於任意once.Do(f) 返回
譯者理解
個人理解,可能存在錯誤,歡迎討論,敬請指教
- [1] 指令重排序是優化手段,編譯器可能會根據上下文重排序語言編譯後的彙編指令,CPU可能會在運行過程中動態分析進行重排序,目的都是爲了減小內存與CPU之間的速度差距。
- [2] 在java的內存模型中,也有happen before原則,事實上,二者是類似的,本質上是一個東西,都是在併發讀寫中規定了共享變量讀寫順序,以保證程序能正確運行。
- [3]
unbuffered channel
相當於volatile關鍵字的Barrier作用,它在兩個併發的goroutine設置了一個同步點,即channel收發之前的事件必然happen before
channel收發之後的事件。