Ref: https://golang.org/ref/memgolang
golang內存模型,主要說明了以下問題。在一個goroutine中讀取變量,而該變量是由其餘goroutine賦值的,這種狀況下如何可以安全正確的讀取。編程
對於有多個goroutine在使用的變量,修改時須要序列化的讀取。緩存
主要方式包括,經過channel的方式、sync/atomic等原子同步操做等。安全
若是你想讀完如下內容,以便理解你的程序內在運行機制,說明你很聰明。併發
可是不建議你這麼聰明~編程語言
只有一個goroutine的時候,讀寫操做都會如程序定義的順序執行。這是由於,儘管編譯器和中央處理器是可能會改變執行順序,但並不會影響編程語言定義的goroutine中的行爲邏輯。但也是由於可能改變執行順序,一樣的操做在不一樣的goroutine中觀察到的執行順序並不一致。好比A goroutine執行了a=1;b=2;另外一個goroutine觀察到的結果多是b先於a被賦值了。函數
爲具體說明讀寫操做的要求,咱們定義了以前某個版本中Golang程序中內存操做的一部分邏輯以下。若是事件e1發生在事件e2以前,咱們說e2發生在e1以後.若是e1並不發生在e2以前,也不發生e2在以後,咱們說e1和e2是同步發生的。atom
在一個單goroutine的程序中,事件發生的順序就是程序描述的順序。spa
知足以下兩個條件的狀況下,對變量v的讀取操做r,是可以觀察到對v的寫入操做w的:線程
爲了保證對變量v對讀取操做r,可以觀察到特定的對v得寫操做w,須要保證w是r惟一可以觀察到寫操做。所以,要保證r可以觀察到w須要知足以下兩個條件:
這對條件是比第一個條件更加嚴格,它要求r和w的同時,沒有其它的寫操做(即和r或w同步的寫操做);
在單一goroutine裏面,沒有同步操做,因此以上兩組條件是等價的。可是對於多goroutine,須要經過同步事件來肯定順序發生,從而保證讀操做可以觀察到寫操做。
在內存模型裏面,變量v初始化爲零值,也是一種寫操做。
讀寫大於單一機器碼的變量的動做,實際操做順序不定,
程序初始化在單一goroutine裏面,可是goroutine會建立其餘goroutine,而後多個goroutine同步執行。
若是package p引用了package q,q的init函數的執行,會先於全部p的函數執行。
Main.main函數的執行,在全部init函數執行完後。
go表達式,會建立一個goroutine,而後該goroutine才能開始執行。
var a string func f() { print(a) } func hello() { a = "hello, world" go f() }
以上代碼示例,調用hello函數,可能在hello已經return到時候,f纔回執行print。
goroutine的退出時機並無保證必定會在某個事件以前。
var a string
func hello() {
go func() { a = "hello" }()
print(a)
}
好比以上示例,對a的賦值,並不保證與hello自己的任何動做保持同步關係,因此也不能保證被其餘任何goroutine的讀操做觀察到。事實上,任何一個激進的編譯器都會把這裏整個go表達式直接刪掉,不作編譯。
若是一個goroutine的影響想被其餘的goroutine觀察到,必須經過同步機制(好比鎖、channel)來肯定相對順序關係。
channel通訊是goroutines之間主要的同步方式。通常來講channel上的每一次send都會相應有另外一個goroutine今後channel受到消息。
同一個channel上,send操做老是先於相應的receive操做完成。
var c = make(chan int, 10) var a string func f() { a = "hello, world" c <- 0 } func main() { go f() <-c print(a) }
以上示例,可以保證print出『hello, world』。對a的寫,是先於往c中發送0,而從c中接收值,先於print。
channel的關閉,先於接收到該channel關閉而發出來的0.
在上面這個例子中,用close(c)代替 c<-0,其效果是同樣的。
對於沒有緩存的channel,receive發生在send完成以前。
var c = make(chan int) var a string func f() { a = "hello, world" <-c } func main() { go f() c <- 0 print(a) }
以上示例,依舊可以保證print出『hello, world』。對a的寫,先於從c接收;從c接收,先於 c <- 0執行完; c <- 0執行完,先於print執行。
但若是channel是緩存的(例如c = make(chan int, 1)),那麼以上程序不能保證print出『hello, world』,甚至有可能出現空值、crash等狀況;
對於緩存容量爲C的channel,第k次接收,先於K+C次發送完成。
這條規則歸納了緩存和非緩存channel的規則。所以基於帶緩存的channel,能夠實現令牌策略:在channel中緩存的數量表明active的數量;channel的緩存容量表示最大可使用的數量;發送消息表示申請了一個令牌,接收消息表示釋放了一塊令牌。這是限制併發經常使用的一種手段。
var limit = make(chan int, 3) func main() { for _, w := range work { go func(w func()) { limit <- 1 w() <-limit }(w) } select{} }
以上示例程序,對於work list中的每一條,都建立了一個goroutine,可是用limit這個帶緩存的channel來限制了,最多同時只能有3個goroutines來執行work方法。
sync包中實現了兩個鎖的數據類型,分別是sync.Mutex和sync.RWMutex
對於任何的sync.Mutex和sync.RWMutex類型變量l,和n<m,對於l.Unlock()的調用n,老是先於對於l.Lock()的調用m。
var l sync.Mutex var a string func f() { a = "hello, world" l.Unlock() } func main() { l.Lock() go f() l.Lock() print(a) }
如上示例可以保證print出『hello, world』。f中第一個l.Unlock()的調用,先於main中第二個l.Lock()的調用;第二個l.Lock()的調用先於print的調用;
任何對於l.Rlock的調用(其中l爲sync.RWMutex類型變量),老是有一個n,l.Lock在調用n執行l.Unlock以後才能return;對應的,l.RUnlock的執行在調用n+1執行l.Unlock以前。
Sync包提供了一種安全的多goroutine種初始化機制,那就是Once類型。對於特定的方法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() }
這裏print兩次『hello, world』,但只有第一次調用doprint會執行setup賦值。
對於同步發生的讀操做r和寫操做w,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.這個事實顛覆了咱們的一些習慣認知。
對於同步問題加鎖必定要double check。
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()
}
如這個例子,不能保證觀察到done的寫操做時候,也能觀察到對a的寫操做。其中一個goroutine可能打印出空字符串。
另一種錯誤的典型以下:
var a string var done bool func setup() { a = "hello, world" done = true } func main() { go setup() for !done { } print(a) }
同上一個例子同樣,這裏對done得寫觀察,不能保證對a的寫觀察,因此也可能打印出空字符串。
更甚,因爲main和setup兩個線程間沒有同步事件,並不能保證main中必定能觀察到done的寫操做,所以main中的一直循環下去沒有結束。(這裏不是很理解,只能說setup的執行時機和main中for循環沒有明確的相對前後和相對距離,因此可能致使循環好久setup還沒執行,或執行了可是沒有更新到main所讀取的done)
還有以上風格的一些變體,以下:
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的賦值而退出循環,可是也不能保證觀察到g.msg的初始化值。
對於以上全部例子,解決方案是同樣的,定義明確的同步機制。