內(nèi)存模型是非常重要的,理解Go的內(nèi)存模型會(huì)就可以明白很多奇怪的競(jìng)態(tài)條件問題,"The Go Memory Model"的原文在這里,讀個(gè)四五遍也不算多。
這里并不是要翻譯這篇文章,英文原文是精確的,但讀起來卻很晦澀,尤其是happens-before的概念本身就是不好理解的,很容易跟時(shí)序問題混淆。大多數(shù)讀者第一遍讀Go的內(nèi)存模型時(shí)基本上看不懂它在說什么。所以我要做的事情用不怎么精確但相對(duì)通俗的語言解釋一下。
先用一句話總結(jié),Go的內(nèi)存模型描述的是"在一個(gè)groutine中對(duì)變量進(jìn)行讀操作能夠偵測(cè)到在其他goroutine中對(duì)該變量的寫操作"的條件。
為了證明這個(gè)重要性,先看一個(gè)例子。下面一小段代碼:
package main
import (
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
var count int
var ch = make(chan bool, 1)
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
ch <- true
count++
time.Sleep(time.Millisecond)
count--
<-ch
wg.Done()
}()
}
wg.Wait()
}
以上代碼有沒有什么問題?這里把buffered channel作為semaphore來使用,表面上看最多允許一個(gè)goroutine對(duì)count進(jìn)行++和--,但其實(shí)這里是有bug的。根據(jù)Go語言的內(nèi)存模型,對(duì)count變量的訪問并沒有形成臨界區(qū)。編譯時(shí)開啟競(jìng)態(tài)檢測(cè)可以看到這段代碼有問題:
go run -race test.go
編譯器可以檢測(cè)到16和18行是存在競(jìng)態(tài)條件的,也就是count并沒像我們想要的那樣在臨界區(qū)執(zhí)行。繼續(xù)往下看,讀完這一節(jié),回頭再來看就可以明白為什么這里有bug了。
happens-before是一個(gè)術(shù)語,并不僅僅是Go語言才有的。簡單的說,通常的定義如下:
假設(shè)A和B表示一個(gè)多線程的程序執(zhí)行的兩個(gè)操作。如果A happens-before B,那么A操作對(duì)內(nèi)存的影響 將對(duì)執(zhí)行B的線程(且執(zhí)行B之前)可見。
無論使用哪種編程語言,有一點(diǎn)是相同的:如果操作A和B在相同的線程中執(zhí)行,并且A操作的聲明在B之前,那么A happens-before B。
int A, B;
void foo()
{
// This store to A ...
A = 5;
// ... effectively becomes visible before the following loads. Duh!
B = A * A;
}
還有一點(diǎn)是,在每門語言中,無論你使用那種方式獲得,happens-before關(guān)系都是可傳遞的:如果A happens-before B,同時(shí)B happens-before C,那么A happens-before C。當(dāng)這些關(guān)系發(fā)生在不同的線程中,傳遞性將變得非常有用。
剛接觸這個(gè)術(shù)語的人總是容易誤解,這里必須澄清的是,happens-before并不是指時(shí)序關(guān)系,并不是說A happens-before B就表示操作A在操作B之前發(fā)生。它就是一個(gè)術(shù)語,就像光年不是時(shí)間單位一樣。具體地說:
這兩個(gè)陳述看似矛盾,其實(shí)并不是。如果你覺得很困惑,可以多讀幾篇它的定義。后面我會(huì)試著解釋這點(diǎn)。記住,happens-before 是一系列語言規(guī)范中定義的操作間的關(guān)系。它和時(shí)間的概念獨(dú)立。這和我們通常說”A在B之前發(fā)生”時(shí)表達(dá)的真實(shí)世界中事件的時(shí)間順序不同。
這里有個(gè)例子,其中的操作具有happens-before關(guān)系,但是實(shí)際上并不一定是按照那個(gè)順序發(fā)生的。下面的代碼執(zhí)行了(1)對(duì)A的賦值,緊接著是(2)對(duì)B的賦值。
int A = 0;
int B = 0;
void main()
{
A = B + 1; // (1)
B = 1; // (2)
}
根據(jù)前面說明的規(guī)則,(1) happens-before (2)。但是,如果我們使用gcc -O2編譯這個(gè)代碼,編譯器將產(chǎn)生一些指令重排序。有可能執(zhí)行順序是這樣子的:
將B的值取到寄存器
將B賦值為1
將寄存器值加1后賦值給A
也就是到第二條機(jī)器指令(對(duì)B的賦值)完成時(shí),對(duì)A的賦值還沒有完成。換句話說,(1)并沒有在(2)之前發(fā)生!
那么,這里違反了happens-before關(guān)系了嗎?讓我們來分析下,根據(jù)定義,操作(1)對(duì)內(nèi)存的影響必須在操作(2)執(zhí)行之前對(duì)其可見。換句話說,對(duì)A的賦值必須有機(jī)會(huì)對(duì)B的賦值有影響.
但是在這個(gè)例子中,對(duì)A的賦值其實(shí)并沒有對(duì)B的賦值有影響。即便(1)的影響真的可見,(2)的行為還是一樣。所以,這并不能算是違背happens-before規(guī)則。
下面這個(gè)例子中,所有的操作按照指定的順序發(fā)生,但是并能不構(gòu)成happens-before 關(guān)系。假設(shè)一個(gè)線程調(diào)用pulishMessage,同時(shí),另一個(gè)線程調(diào)用consumeMessage。 由于我們并行的操作共享變量,為了簡單,我們假設(shè)所有對(duì)int類型的變量的操作都是原子的。
int isReady = 0;
int answer = 0;
void publishMessage()
{
answer = 42; // (1)
isReady = 1; // (2)
}
void consumeMessage()
{
if (isReady) // (3) <-- Let's suppose this line reads 1
printf("%d\n", answer); // (4)
}
根據(jù)程序的順序,在(1)和(2)之間存在happens-before 關(guān)系,同時(shí)在(3)和(4)之間也存在happens-before關(guān)系。
除此之外,我們假設(shè)在運(yùn)行時(shí),isReady讀到1(是由另一個(gè)線程在(2)中賦的值)。在這中情形下,我們可知(2)一定在(3)之前發(fā)生。但是這并不意味著在(2)和(3)之間存在happens-before 關(guān)系!
happens-before 關(guān)系只在語言標(biāo)準(zhǔn)中定義的地方存在,這里并沒有相關(guān)的規(guī)則說明(2)和(3)之間存在happens-before關(guān)系,即便(3)讀到了(2)賦的值。
還有,由于(2)和(3)之間,(1)和(4)之間都不存在happens-before關(guān)系,那么(1)和(4)的內(nèi)存交互也可能被重排序 (要不然來自編譯器的指令重排序,要不然來自處理器自身的內(nèi)存重排序)。那樣的話,即使(3)讀到1,(4)也會(huì)打印出“0“。
我們回過頭來再看看"The Go Memory Model"中關(guān)于happens-before的部分。
如果滿足下面條件,對(duì)變量v的讀操作r可以偵測(cè)到對(duì)變量v的寫操作w:
為了保證對(duì)變量v的讀操作r可以偵測(cè)到某個(gè)對(duì)v的寫操作w,必須確保w是r可以偵測(cè)到的唯一的寫操作。也就是說當(dāng)滿足下面條件時(shí)可以保證讀操作r能偵測(cè)到寫操作w:
關(guān)于channel的happens-before在Go的內(nèi)存模型中提到了三種情況:
先看一個(gè)簡單的例子:
var c = make(chan int, 10)
var a string
func f() {
a = "hello, world" // (1)
c <- 0 // (2)
}
func main() {
go f()
<-c // (3)
print(a) // (4)
}
上述代碼可以確保輸出"hello, world",因?yàn)?1) happens-before (2),(4) happens-after (3),再根據(jù)上面的第一條規(guī)則(2)是 happens-before (3)的,最后根據(jù)happens-before的可傳遞性,于是有(1) happens-before (4),也就是a = "hello, world" happens-before print(a)。
再看另一個(gè)例子:
var c = make(chan int)
var a string
func f() {
a = "hello, world" // (1)
<-c // (2)
}
func main() {
go f()
c <- 0 // (3)
print(a) // (4)
}
根據(jù)上面的第三條規(guī)則(2) happens-before (3),最終可以保證(1) happens-before (4)。
如果我把上面的代碼稍微改一點(diǎn)點(diǎn),將c變?yōu)橐粋€(gè)帶緩存的channel,則print(a)打印的結(jié)果不能夠保證是"hello world"。
var c = make(chan int, 1)
var a string
func f() {
a = "hello, world" // (1)
<-c // (2)
}
func main() {
go f()
c <- 0 // (3)
print(a) // (4)
}
因?yàn)檫@里不再有任何同步保證,使得(2) happens-before (3)??梢曰仡^分析一下本節(jié)最前面的例子,也是沒有保證happens-before條件。
更多建議: