介紹
Go 的內(nèi)存模型是可以讓多個(gè) goroutine 共享數(shù)據(jù)的,但指定了條件,在這種條件下,保證一個(gè) goroutine 中可以讀取另一個(gè) goroutine 中寫入的變量值。
建議
程序中有多個(gè) goroutine 同時(shí)去更新數(shù)據(jù)時(shí),必須用序列化的方式。比如用 channel 操作或 sync、sync/atomic 的同步原語(yǔ)。
意思是:需要同步的場(chǎng)景,使用顯式的同步!顯式的同步??!顯式的?。?!
Happens Before
Happens-before 是一個(gè)順序規(guī)范,準(zhǔn)確地說(shuō)是規(guī)定了部分順序,讓某些事件發(fā)生在另一些事件之前。
單個(gè) goroutine
在單個(gè) goroutine 中,讀和寫操作必須 表現(xiàn)得像 是他們是按程序的執(zhí)行順序進(jìn)行的。
也就是說(shuō),實(shí)際上,在單個(gè) goroutine 中編譯器和執(zhí)行器會(huì)對(duì)讀操作和寫操作的順序進(jìn)行重排,但它保證了這個(gè)重排不會(huì)改變 語(yǔ)言規(guī)范 中所定義的行為。
由于重排,一個(gè) goroutine 觀察到的執(zhí)行順序可能與另一個(gè) goroutine 的不同。
比如,一個(gè) goroutine 執(zhí)行 a = 1; b = 2,那另一個(gè) goroutine 可能先觀察到 b == 2 而此時(shí) a != 1。如下測(cè)試代碼:
var count = new(uint64)
func main() {
for i := 0; i < 1000000; i++ {
test()
}
time.Sleep(time.Second)
fmt.Println(*count) // 輸出不為 0
}
func test() {
var a, b int
go func() {
A, B := a, b
if B == 2 && A != 1 {
atomic.AddUint64(count, 1)
}
}()
go func() {
a = 1
b = 2
}()
}
為了指定讀和寫的要求,我們?cè)?Go 中定義了 Happens before 的概念——Go 執(zhí)行內(nèi)存操作的部分順序。
如果事件 e1 在 e2 之前發(fā)生,我們可以說(shuō) e2 發(fā)生在 e1 之后。如果 e1 即不在 e2 之前發(fā)生也不在 e2 之后發(fā)生,我們稱為 e1 和 e2 是同時(shí)發(fā)生。
在單 goroutine 中,happens-before 順序就是程序語(yǔ)言所表達(dá)的順序。
當(dāng)滿足以下兩個(gè)條件時(shí),一個(gè)變量(v)的讀操作(r)允許觀察它的寫操作(w):
- 讀不發(fā)生在寫之前;
- 在此寫操作之后或此讀之前沒(méi)有其他的寫操作(w')發(fā)生。
為了保證上述讀操作(r)能觀察到上述的寫操作(w),即需要保證以下兩個(gè)條件:
- 寫發(fā)生在讀之前;
- 其他任何對(duì) v 的寫操作(w', w''...),或者發(fā)生在 w 之前,或者發(fā)生在 r 之后。
這兩個(gè)條件比上面的兩個(gè)條件更健壯。它要求了這里沒(méi)有其他的寫操作與這里的 r 和 w 并發(fā)。
在單協(xié)程中沒(méi)有并發(fā),所以讀操作(r)總能觀察到最近一次寫操作(w)的值。
而在多協(xié)程中,我們就必須用同步事件來(lái)構(gòu)建 happens-before 條件來(lái)確保讀觀察到了期望的寫入。
對(duì)于一個(gè)變量初始化為它的零值時(shí),表現(xiàn)為在內(nèi)存模型中的一個(gè)寫操作。
對(duì)一個(gè)大于 單機(jī)器字 的讀和寫操作表現(xiàn)為對(duì)一個(gè)未指明順序的 多機(jī)器字大小(multiple machine-word-sized)的操作。
同步(Synchronization)
初始化(Initialization)
- 程序初始會(huì)運(yùn)行一個(gè)單協(xié)程,但這個(gè)協(xié)程可能會(huì)創(chuàng)建并發(fā)執(zhí)行的其他協(xié)程。
- 如果 p 包引入了 q 包,那 q 的
init()函數(shù)發(fā)生在 p 的任何操作之前(包括 p 的init())。 -
main.main()(main 包下的main())發(fā)生在所有的init()函數(shù)之后。
協(xié)程的創(chuàng)建
- go 命令開(kāi)啟一個(gè) go 協(xié)程發(fā)生在此 go 協(xié)程執(zhí)行之前。(即:先創(chuàng)建再執(zhí)行。這句話的意思是說(shuō) go 協(xié)程創(chuàng)建之前的語(yǔ)句和這個(gè) go 協(xié)程沒(méi)有并發(fā)問(wèn)題)
協(xié)程的銷毀
- 協(xié)程的退出不保證發(fā)生在程序的任何事情之前。(有點(diǎn)拗口,就是指協(xié)程的執(zhí)行時(shí)間和退出正常情況下完全獨(dú)立,沒(méi)有任何時(shí)間保證)
(我們初學(xué)協(xié)程時(shí),常常會(huì)寫 main 函數(shù)的最后一句創(chuàng)建 go 協(xié)程,結(jié)果導(dǎo)致主程序結(jié)束了協(xié)程還沒(méi)執(zhí)行。就是這個(gè)意思)
確保并發(fā)下同步的三種方式
用 channel
- 向 channel 中寫數(shù)據(jù)發(fā)生在 對(duì)應(yīng) 的讀操作之前(即讀時(shí)保證先完成寫操作);
- channel 的關(guān)閉操作發(fā)生在讀操作之前,由于通道關(guān)閉,返回它對(duì)應(yīng)類型的零值;
- 從無(wú)緩沖的 channel 中讀操作,發(fā)生在向此 channel 的寫完成之前;
用鎖
sync 包提供了兩種鎖 sync.Mutex 和 sync.RWMutex
-
Lock()之后,并發(fā)協(xié)程中的Unlock()一定發(fā)生在其他協(xié)程Lock()之前
用 Once
var once sync.Once
...
func doprint() {
once.Do(setup)
print(setup)
}
- Once 保證了
once.Do()只執(zhí)行一次
總結(jié)
這里注意的內(nèi)容主要有以下幾點(diǎn):
- Go 的 goroutine 是共享內(nèi)存的;
- Happends-before 原則;
- 編譯器和執(zhí)行器會(huì)對(duì)編碼的執(zhí)行順序進(jìn)行重排,但在單協(xié)程中對(duì)外表現(xiàn)一致;
- 學(xué)會(huì)用幾種方式保證協(xié)程中讀寫的順序(與自己期望的一致);
- 使用顯示的同步做同步!??!
不正確的同步:
var a string
var done bool
func setup() {
a = "hello, world" //
done = true // 由于這兩個(gè)的賦值操作不一定那個(gè)先
}
func main() {
go setup()
for !done {
}
print(a)
}
附:
- 官方文檔地址The Go Membery Model