更好的閱讀體驗建議點擊下方原文鏈接。
原文鏈接:http://maoqide.live/post/golang/the-go-memory-model/
[譯]https://golang.google.cn/ref/mem
Go內存模型指定了一個條件,在該條件下,可以保證在一個 goroutine 中讀取變量,能夠獲取到另一個不同 goroutine 寫入同一變量產生的值。
Introduction
Go內存模型指定了一個條件,在該條件下,可以保證在一個 goroutine 中讀取變量,能夠獲取到另一個不同 goroutine 寫入同一變量產生的值。
Advice
如果一個程序要修改被多個 goroutine 同時訪問的數據,必須序列化此類訪問。
要序列化訪問,請使用 channel 操作或其他同步原語(例如sync
和sync/atomic
包中的那些)來保護數據。
如果您必須閱讀本文檔的其余部分以了解程序的行為,那么您就太聰明了。
別聰明。
Happens Before
在單個 goroutine 中,讀取和寫入必須表現得好像它們按程序指定的順序執行。也就是說,只有當重新排序不改變語言規范中定義的 goroutine 的行為時,編譯器和處理器才可以對在單個 goroutine 中讀取和寫入操作的執行進行重新排序。由於這種重新排序,一個 goroutine 觀察到的執行順序可能與另一個 goroutine 感知到的順序不同。例如,如果一個 goroutine 執行a = 1; b = 2;
,另一個 goroutine 可能會在 a 的值更新之前觀察到 b 的更新值。
為了指定讀取和寫入的要求,我們定義 發生之前(hanppen before),Go 程序中內存操作的局部順序。如果事件 e1 在事件 e2 發生之前(hanppen before),那么我們說事件 e2 在事件 e1 發生之后(hanppen after)。另外,如果 e1 在 e2 之前沒有發生並且在 e2 之后沒有發生,那么我們說 e1 和 e2 同時發生。
在單個goroutine中,happens-before
順序是程序表達的順序。
如果以下兩個都成立,則允許變量 v 的讀取操作 r 觀察到寫入操作 w 寫入到 v 的值:
1. r 沒有發生在 w 之前。
2. 在 w 之后但在 r 之前沒有其他寫入操作 w'。
為了保證對變量 v 的讀取操作 r 觀察到特定寫入操作 w 對 v 寫入的值,確保 w 是允許讀取操作 r 觀察到的唯一的寫入操作。也就是說,如果以下兩個條件都成立,才能保證讀取操作 r 能夠觀察到寫入操作 w:
1. w 發生在 r 之前。
2. 任何其他對共享變量 v 的寫入操作,要么發生在 w 之前,要么發生在 r 之后。
這組條件比第一組更加嚴格。它要求沒有其他寫入與 w 或 r 同時發生。
在單個goroutine中,沒有並發,因此這兩個定義是等效的:一個讀取操作 r 觀察最近的寫入操作 w 寫入 v 的值。
具有零值的 v 的類型的變量 v 的初始化表現為以上存儲模型中的寫入。
對於大於單個機器字的值的讀取和寫入操作,表現為以未指定順序進行的多個 機器字大小的操作。
Synchronization
Initialization
程序的初始化在單個 goroutine 中運行,但該 goroutine 可能會創建其他並發運行的 goroutine。
如果包 p 導入包 q,則 q 的 init 函數在包 p 的任何代碼開始之前完成。
函數 main.main 在所有的 init 函數完成后開始執行。
Goroutine creation
啟動新 goroutine 的 go 語句發生在該 goroutine 開始執行之前。
例如,在此程序中:
var a string
func f() {
print(a)
}
func hello() {
a = "hello, world"
go f()
}
調用hello
將在未來的某個時刻打印“hello,world”(也許在hello
返回之后)。
Goroutine destruction
goroutine 的退出不保證在程序中的任何事件之前發生。例如,在此程序中:
var a string
func hello() {
go func() { a = "hello" }()
print(a)
}
對 a 的賦值沒有伴隨任何同步事件,因此不保證任何其他 goroutine 都能觀察到它。事實上,一個激進的編譯器可能會刪掉整條 go 語句。
如果一個 goroutine 影響必須被另一個 goroutine 觀察到,要使用鎖或 channel 通信等同步機制來建立相對順序。
Channel communication
channel 通信是 goroutine 之間同步的主要方法。特定 channel 上的每一個 send 操作都與該 channel 對應的 receive 操作相匹配,通常在不同的 goroutine 中。
channel 的 send 在該 channel 相應的 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 的相應的 receive 完成之前,即發生在print
之前。
channel 的關閉發生在因通道已關閉而接收到零值返回之前
在前面的示例中,用 close(c)
替換c <- 0
會產生具有保證同樣行為的程序。
無緩沖 channel 的 receive 操作在該 channel 的 send 操作完成之前發生。
示例程序(和上面一樣,但是交換了 send 和 receive 語句並且使用了無緩沖的 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 的寫入發生在 c 的 receive 之前,即發生在 c 的相應的 send 完成之前,即發生在print
之前。
如果 channel 是有緩沖的,(例如,c = make(chan int, 1)),那么程序將不能保證打印 "hello, world"。(可能會打印空字符串,崩潰或執行其他操作。)
具有容量C
的 channel 的第 k
次 receive 操作,在第 k+C
次 send 操作完成之前。
此規則概括了先前的有緩沖的 channel 的規則。它允許用有緩沖的 channel 建立的計數信號量:channel 中的 data 數量對應於當前的使用數量,channel 的容量對應於允許最大同時使用的數量,發送一條 data 來獲取信號量,接收一條 data 來釋放信號量。這是限制並發數量的常用用法。
該程序為工作列表中的每個條目啟動一個 goroutine,但是 goroutine 利用limit
這個 channel 來確保一次最多有三個正在運行的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.Unock()
在第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()
(在f()中),在第二次調用l.Lock()
(在main()中)返回之前發生,即在print
之前發生。
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()
的任意調用,有一個 n 使得本次l.RLock()
在第 n 次調用l.Unlock()
之后發生(返回)並且對應的l.RUnlock()
在第 n+1 調用l.Lock()
之前發生。
Once
sync
包通過使用Once類型,在存在多個 goroutine 的情況下提供了一種安全的初始化機制。多個線程可以對特定的 f 執行nce.Do(f)
,但是只有一個線程會真正運行f()
,並且其他調用會阻塞直到f()
返回。
從once.Do(f)
中對f()
的單次調用在任意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 方法將在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()
}
可能會發生 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
中,觀察到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
能觀察到對done
的寫入操作,因為兩個線程之間沒有同步事件。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
觀察到g != nil
並且退出循環,無法保證它會觀察到g.msg
的初始化值。
在所有這些示例中,解決方案是相同的:使用顯式的同步。