轉載請註明出處,原文連接http://tailnode.tk/2017/01/Go...node
翻譯自The Go Memory Modelgolang
如何保證在一個goroutine中看到在另外一個goroutine修改的變量的值,這篇文章進行了詳細說明。安全
若是程序中修改數據時有其餘goroutine同時讀取,那麼必須將讀取串行化。爲了串行化訪問,請使用channel或其餘同步原語,例如sync和sync/atomic來保護數據。併發
在一個gouroutine中,讀和寫必定是按照程序中的順序執行的。即編譯器和處理器只有在不會改變這個goroutine的行爲時纔可能修改讀和寫的執行順序。因爲重排,不一樣的goroutine可能會看到不一樣的執行順序。例如,一個goroutine執行a = 1;b = 2;
,另外一個goroutine可能看到b
在a
以前更新。
爲了說明讀和寫的必要條件,咱們定義了先行發生(Happens Before)
--Go程序中執行內存操做的偏序。若是事件e1
發生在e2
前,咱們能夠說e2
發生在e1
後。若是e1
不發生在e2
前也不發生在e2
後,咱們就說e1
和e2
是併發的。
在單獨的goroutine中先行發生的順序便是程序中表達的順序。
當下麪條件知足時,對變量v的讀操做r是被容許看到對v的寫操做w的:app
r不先行發生於w函數
在w後r前沒有對v的其餘寫操做atom
爲了保證對變量v的讀操做r看到對v的寫操做w,要確保w是r容許看到的惟一寫操做。即當下麪條件知足時,r 被保證看到w:線程
w先行發生於r翻譯
其餘對共享變量v的寫操做要麼在w前,要麼在r後。
這一對條件比前面的條件更嚴格,須要沒有其餘寫操做與w或r併發發生。code
單獨的goroutine中沒有併發,因此上面兩個定義是相同的:讀操做r看到最近一次的寫操做w寫入v的值。當多個goroutine訪問共享變量v時,它們必須使用同步事件來創建先行發生這一條件來保證讀操做能看到須要的寫操做。
對變量v的零值初始化在內存模型中表現的與寫操做相同。
對大於一個字的變量的讀寫操做表現的像以不肯定順序對多個一字大小的變量的操做。
程序的初始化在單獨的goroutine中進行,但這個goroutine可能會建立出併發執行的其餘goroutine。
若是包p引入(import)包q,那麼q的init函數的結束先行發生於p的全部init函數開始
main.main函數的開始發生在全部init函數結束以後
go
關鍵字開啓新的goroutine,先行發生於這個goroutine開始執行,例以下面程序:
var a string func f() { print(a) } func hello() { a = "hello, world" go f() }
調用hello
會在以後的某時刻打印出"hello, world"(可能在hello
返回以後)
gouroutine的退出並不會保證先行發生於程序的任何事件。例以下面程序:
var a string func hello() { go func() { a = "hello" }() print(a) }
沒有用任何同步操做限制對a的賦值,因此並不能保證其餘goroutine能看到a的變化。實際上,一個激進的編譯器可能會刪掉整個go語句。
若是想要在一個goroutine中看到另外一個goroutine的執行效果,請使用鎖或者channel這種同步機制來創建程序執行的相對順序。
channel通訊是goroutine同步的主要方法。每個在特定channel的發送操做都會匹配到一般在另外一個goroutine執行的接收操做。
在channel的發送操做先行發生於對應的接收操做完成
例如:
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上的發送,先行發生於在c上的對應的接收完成,先行發生於print
。
對channel的關閉先行發生於接收到零值,由於channel已經被關閉了。
在上面的例子中,將c <- 0
替換爲close(c)
還會產生一樣的結果。
無緩衝channel的接收先行發生於發送完成
以下程序(和上面相似,只交換了對channel的讀寫位置並使用了非緩衝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接收,先行發生於向c發送完成,先行發生於print
。
若是是帶緩衝的channel(例如c = make(chan int, 1)
),程序不保證打印出"hello, world"(可能打印空字符,程序崩潰或其餘行爲)。
在容量爲C的channel上的第k個接收先行發生於從這個channel上的第k+C次發送完成。
這條規則將前面的規則推廣到了帶緩衝的channel上。能夠經過帶緩衝的channel來實現計數信號量:channel中的元素數量對應着活動的數量,channel的容量表示同時活動的最大數量,發送元素獲取信號量,接收元素釋放信號量,這是限制併發的一般用法。
下面程序爲work
中的每一項開啓一個goroutine,但這些goroutine經過有限制的channel來確保最多同時執行三個工做函數(w)。
var limit = make(chan int, 3) func main() { for _, w := range work { go func(w func()) { limit <- 1 w() <-limit }(w) } select{} }
sync包實現了兩個鎖的數據類型sync.Mutex和sync.RWMutex。
對任意的sync.Mutex或sync.RWMutex變量l和n < m,n次調用l.Unlock()先行發生於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()中)先行發生於main中的第二次l.Lock()返回, 先行發生於print。
對於sync.RWMutex變量l,任意的函數調用l.RLock知足第n次l.RLock後發生於第n次調用l.Unlock,對應的l.RUnlock先行發生於第n+1次調用l.Lock。
sync包的Once
爲多個goroutine提供了安全的初始化機制。能在多個線程中執行once.Do(f)
,但只有一個f()
會執行,其餘調用會一直阻塞直到f()
返回。
經過once.Do(f)
執行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
會打印"hello, world"兩次。setup
只在第一次doprint
時執行。
注意,讀操做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的初始化值。 在上面全部的例子中,解決辦法都是相同的:明確的使用同步。