很多編譯器優(yōu)化(在編譯時(shí)刻)和CPU處理器優(yōu)化(在運(yùn)行時(shí)刻)會(huì)常常調(diào)整指令執(zhí)行順序,從而使得指令執(zhí)行順序和代碼中指定的順序不太一致。 指令順序也稱為內(nèi)存順序。
當(dāng)然,指令執(zhí)行順序的調(diào)整規(guī)則不是任意的。 最基本的要求是發(fā)生在一個(gè)不與其它協(xié)程共享數(shù)據(jù)的協(xié)程內(nèi)部的指令執(zhí)行順序調(diào)整在此協(xié)程內(nèi)部必須不能被覺察到。 換句話說,從這樣的一個(gè)協(xié)程的角度看,其中的指令執(zhí)行順序和代碼中指定的順序總是一致的,即使確實(shí)有一些指令的執(zhí)行順序發(fā)生了調(diào)整。
然而,如果一些協(xié)程之間共享數(shù)據(jù),那么在其中一個(gè)協(xié)程中發(fā)生的指令執(zhí)行順序調(diào)整將有可能被剩余的其它協(xié)程覺察到,從而影響到所有這些協(xié)程的行為。 在并發(fā)編程中,多個(gè)協(xié)程之間共享數(shù)據(jù)是家常便飯。 如果我們忽視了指令執(zhí)行順序調(diào)整帶來的影響,我們編寫的并發(fā)程序的行為將依賴于特定編譯器和CPU。這樣的程序常常表現(xiàn)出異常行為。
下面是一個(gè)編寫得非常不職業(yè)的Go程序。此程序的編寫沒有考慮指令執(zhí)行順序調(diào)整帶來的影響。 此程序擴(kuò)展于官方文檔Go 1 內(nèi)存模型一文中的一個(gè)例子.
package main
import "log"
import "runtime"
var a string
var done bool
func setup() {
a = "hello, world"
done = true
if done {
log.Println(len(a)) // 如果被打印出來,它總是12
}
}
func main() {
go setup()
for !done {
runtime.Gosched()
}
log.Println(a) // 期待的打印結(jié)果:hello, world
}
此程序的行為很可能正如我們所料,hello, world
將被打印輸出。 然而,此程序的行為并非跨編譯器和跨平臺(tái)架構(gòu)兼容的。 如果此程序使用一個(gè)不同的(但符合Go規(guī)范的)編譯器或者不同的編譯器版本編譯,它的運(yùn)行結(jié)果可能是不同的。 即使此程序使用同一個(gè)編譯器編譯,在不同的平臺(tái)架構(gòu)上的運(yùn)行結(jié)果也可能是不同的。
編譯器和CPU可能調(diào)整setup
函數(shù)中的前兩條語句的執(zhí)行順序,使得setup
協(xié)程中的指令的執(zhí)行順序和下面的代碼指定的順序一致。
func setup() {
done = true
a = "hello, world"
if done {
log.Println(len(a))
}
}
setup
協(xié)程并不會(huì)覺察到此執(zhí)行順序調(diào)整,所以此協(xié)程中的log.Println(len(a))
語句將總是打印出12
(如果此打印語句在程序退出之前得到了執(zhí)行的話)。 但是,此執(zhí)行順序調(diào)整將被主協(xié)程覺察到,所以最終的打印結(jié)果有可能是空,而不是hello, world
。
除了沒有考慮指令執(zhí)行順序調(diào)整帶來的影響,此程序還存在數(shù)據(jù)競爭的問題。 變量a
和done
的使用沒有進(jìn)行同步。 所以此程序是一個(gè)充滿了各種并發(fā)編程錯(cuò)誤的不良例子。 一個(gè)職業(yè)的Go程序員不應(yīng)該寫出這樣的使用于生產(chǎn)環(huán)境中的代碼。
我們可以使用Go官方工具鏈中的go build -race
命令來編譯并運(yùn)行一個(gè)程序,以檢查此程序中是否存在著數(shù)據(jù)競爭。
有時(shí),為了程序的邏輯正確性,我們需要確保一個(gè)協(xié)程中的一些語句一定要在另一個(gè)協(xié)程的某些語句之后(或者之前)執(zhí)行(從這兩個(gè)協(xié)程的角度觀察都是如此)。 指令執(zhí)行順序調(diào)整可能會(huì)給此需求帶來一些麻煩。 我們應(yīng)如何防止某些可能發(fā)生的指令執(zhí)行順序調(diào)整呢?
不同的CPU架構(gòu)提供了不同的柵欄(fence)指令來防止各種指令執(zhí)行順序調(diào)整。 一些編程語言提供對應(yīng)的函數(shù)來在代碼中的合適位置插入各種柵欄指令。 但是,理解和正確地使用這些柵欄指令極大地提高了并發(fā)編程的門檻。
Go語言的設(shè)計(jì)哲學(xué)是用盡量少的易于理解的特性來支持盡量多的使用場景,同時(shí)還要盡量保證代碼的高執(zhí)行效率。 所以Go內(nèi)置和標(biāo)準(zhǔn)庫并沒有提供直接插入各種柵欄指令的途徑。 事實(shí)上,這些柵欄指令被使用在Go中提供的各種高級(jí)數(shù)據(jù)同步技術(shù)的實(shí)現(xiàn)中。 所以,我們應(yīng)該使用Go中提供的高級(jí)數(shù)據(jù)同步技術(shù)來保證我們所期待的代碼執(zhí)行順序。
本文的余下部分將列出Go中保證的各種代碼執(zhí)行順序。 這些順序保證可能在Go 1 內(nèi)存模型一文提到了,也可能沒有提到。
在下面的敘述中,如果我們說事件A
保證發(fā)生在事件B
之前,這意味著這兩個(gè)事件涉及到的任何協(xié)程都將觀察到在事件A
之前的語句肯定將在事件B
之后的語句先執(zhí)行。 對于不相關(guān)的協(xié)程,它們所觀察到的順序可能并非如此所述。
在下面這個(gè)函數(shù)中,對x
和y
的賦值保證發(fā)生在對它們的打印之前,并且對x
的打印肯定發(fā)生在對y
的打印之前。
var x, y int
func f1() {
x, y = 123, 789
go func() {
fmt.Println(x)
go func() {
fmt.Println(y)
}()
}()
}
然而這些順序在下面這個(gè)函數(shù)中是得不到任何保證的。此函數(shù)存在著數(shù)據(jù)競爭。
var x, y int
func f2() {
go func() {
fmt.Println(x) // 可能打印出0、123,或其它值
}()
go func() {
fmt.Println(y) // 可能打印出0、789,或其它值
}()
x, y = 123, 789
}
下面列出的是通道操作做出的基本順序保證:
m == 0
),則此通道上的第n次成功接收操作的開始發(fā)生在此通道上的第n次發(fā)送操作完成之前。事實(shí)上, 對一個(gè)非緩沖通道來說,其上的第n次成功發(fā)送的完成的發(fā)送和其上的第n次成功接收的完成應(yīng)被視為同一事件。
下面這段代碼展示了一個(gè)非緩沖通道上的發(fā)送和接收操作是如何保證特定的代碼執(zhí)行順序的。
func f3() {
var a, b int
var c = make(chan bool)
go func() {
a = 1
c <- true
if b != 1 {
panic("b != 1") // 絕不可能發(fā)生
}
}()
go func() {
b = 1
<-c
if a != 1 {
panic("a != 1") // 絕不可能發(fā)生
}
}()
}
對于函數(shù)f3
中創(chuàng)建的兩個(gè)協(xié)程,下列順序?qū)⒌玫奖WC:
b = 1
肯定在條件b != 1
被估值之前執(zhí)行完畢。a = 1
肯定在條件a != 1
被估值之前執(zhí)行完畢。所以,上例代碼中兩個(gè)協(xié)程中的panic
調(diào)用將永不可能得到執(zhí)行。 做為對比,下面這段代碼中的panic
調(diào)用有可能會(huì)得到執(zhí)行,因?yàn)樯鲜鐾ǖ啦僮飨嚓P(guān)的順序保證對于不相關(guān)的協(xié)程是無效的。
func f4() {
var a, b, x, y int
c := make(chan bool)
go func() {
a = 1
c <- true
x = 1
}()
go func() {
b = 1
<-c
y = 1
}()
// 一個(gè)和上面的通道操作不相關(guān)的協(xié)程。
// 這是一個(gè)不良代碼的例子,它造成了很多數(shù)據(jù)競爭。
go func() {
if x == 1 {
if a != 1 {
panic("a != 1") // 有可能發(fā)生
}
if b != 1 {
panic("b != 1") // 有可能發(fā)生
}
}
if y == 1 {
if a != 1 {
panic("a != 1") // 有可能發(fā)生
}
if b != 1 {
panic("b != 1") // 有可能發(fā)生
}
}
}()
}
這里的新創(chuàng)建的第三個(gè)協(xié)程是一個(gè)和通道c
上的發(fā)送和接收操作不相關(guān)的一個(gè)協(xié)程。 它所觀察到的執(zhí)行順序和其它兩個(gè)新創(chuàng)建的協(xié)程可能是不同的。 條件a != 1
和b != 1
的估值有可能為true
,所以四個(gè)panic
調(diào)用有可能會(huì)得到執(zhí)行。
事實(shí)上,大多數(shù)編譯器的實(shí)現(xiàn)確實(shí)很可能能夠保證上面這個(gè)不良的例子中的四個(gè)panic
調(diào)用永遠(yuǎn)不可能被執(zhí)行,但是,沒有任何Go官方文檔做出了這樣的保證。 此不良例子的執(zhí)行結(jié)果是依賴于不同的編譯器和不同的編譯器版本的。 我們編寫的Go代碼應(yīng)該以Go官方文檔中明確記錄下來的規(guī)則為依據(jù)。
下面是一個(gè)緩沖通道的例子。
func f5() {
var k, l, m, n, x, y int
c := make(chan bool, 2)
go func() {
k = 1
c <- true
l = 1
c <- true
m = 1
c <- true
n = 1
}()
go func() {
x = 1
<-c
y = 1
}()
}
在此例子中,下面的順序得以保證:
k = 1
的執(zhí)行保證在賦值語句y = 1
的執(zhí)行之前結(jié)束。x = 1
的執(zhí)行保證在賦值語句n = 1
的執(zhí)行之前結(jié)束。然而,賦值語句x = 1
的執(zhí)行并不能保證在賦值語句l = 1
和m = 1
的執(zhí)行之前結(jié)束, 賦值語句l = 1
和m = 1
的執(zhí)行也不能保證在賦值語句y = 1
的執(zhí)行之前結(jié)束。
下面是一個(gè)通道關(guān)閉的例子。在這個(gè)例子中,賦值語句k = 1
的執(zhí)行保證在賦值語句y = 1
執(zhí)行之前結(jié)束,但不能保證在賦值語句x = 1
執(zhí)行之前結(jié)束。
func f6() {
var k, x, y int
c := make(chan bool, 1)
go func() {
c <- true
k = 1
close(c)
}()
go func() {
<-c
x = 1
<-c
y = 1
}()
}
Go中和互斥鎖相關(guān)的順序保證:
sync.Mutex
類型或者sync.RWMutex
類型的值m
,第n次成功的m.Unlock()
方法調(diào)用保證發(fā)生在第n+1次m.Lock()
方法調(diào)用返回之前。RWMutex
類型值rw
,如果它的第n次rw.Lock()
方法調(diào)用已成功返回,并且有一個(gè)rw.RLock()
方法調(diào)用保證發(fā)生在此第n次rw.Lock()
方法調(diào)用返回之后,則第n次成功的rw.Unlock()
方法調(diào)用保證發(fā)生在此rw.RLock()
方法調(diào)用返回之前。RWMutex
類型值rw
,如果它的第n次rw.RLock()
方法調(diào)用已成功返回,并且有一個(gè)rw.Lock()
方法調(diào)用保證發(fā)生在此第n次rw.RLock()
方法調(diào)用返回之后,則第m次成功的rw.RUnlock()
方法調(diào)用(其中m <= n
)保證發(fā)生在此rw.Lock()
方法調(diào)用返回之前。在下面這個(gè)例子中,下列順序肯定得到保證。
a = 1
的執(zhí)行保證在賦值語句b = 1
的執(zhí)行之前結(jié)束。
m = 1
的執(zhí)行保證在賦值語句n = 1
的執(zhí)行之前結(jié)束。
x = 1
的執(zhí)行保證在賦值語句y = 1
的執(zhí)行之前結(jié)束。
func fab() {
var a, b int
var l sync.Mutex // or sync.RWMutex
l.Lock()
go func() {
l.Lock()
b = 1
l.Unlock()
}()
go func() {
a = 1
l.Unlock()
}()
}
func fmn() {
var m, n int
var l sync.RWMutex
l.RLock()
go func() {
l.Lock()
n = 1
l.Unlock()
}()
go func() {
m = 1
l.RUnlock()
}()
}
func fxy() {
var x, y int
var l sync.RWMutex
l.Lock()
go func() {
l.RLock()
y = 1
l.RUnlock()
}()
go func() {
x = 1
l.Unlock()
}()
}
注意,在下面這段代碼中,根據(jù)Go官方文檔,賦值語句p = 1
的執(zhí)行并不能保證在賦值語句q = 1
的執(zhí)行之前結(jié)束,盡管多數(shù)編譯器確實(shí)能夠做出這樣的保證。
var p, q int
func fpq() {
var l sync.Mutex
p = 1
l.Lock()
l.Unlock()
q = 1
}
假設(shè)在某個(gè)給定時(shí)刻,一個(gè)可尋址的sync.WaitGroup
值wg
維護(hù)的計(jì)數(shù)不為0,并且有一個(gè)wg.Wait()
方法調(diào)用在此給定時(shí)刻之后調(diào)用。 如果有一組wg.Add(n)
方法調(diào)用在此給定時(shí)刻之后調(diào)用,并且我們可以保證這組調(diào)用中只有最后一個(gè)返回的調(diào)用會(huì)將wg
維護(hù)的計(jì)數(shù)修改為0, 則這組調(diào)用中的每個(gè)調(diào)用保證都發(fā)生在此wg.Wait()
方法調(diào)用返回之前。
注意:調(diào)用wg.Done()
和wg.Add(-1)
是等價(jià)的。
請閱讀對sync.WaitGroup
類型的解釋來獲取如何使用sync.WaitGroup
值。
請閱讀對sync.Once
類型的解釋來獲取sync.Once
值做出的順序保證和如何使用sync.Once
值。
sync.Cond
值出的順序保證有些難以表達(dá)清楚。所以這里就只可意會(huì)不可言傳了。 請閱讀對sync.Cond
類型的解釋來獲取如何使用sync.Cond
值。
從Go 1.19開始,Go 1 內(nèi)存模型正式地說明Go程序中執(zhí)行的原子操作按照順序一致次序(sequentially consistent order)執(zhí)行。 如果一個(gè)原子(寫)操作A的效果被一個(gè)原子(讀)操作B觀察到,則A肯定被同步到B之前執(zhí)行。
按照這個(gè)說法,在下面這個(gè)程序中,對變量b
的原子寫操作肯定發(fā)生在對其讀取結(jié)果為1
的原子原子讀操作之前。 從而使得對變量a
的普通寫操作也發(fā)生于對其的普通讀操作之前。 所以此程序保證會(huì)打印出1
。
package main
import (
"fmt"
"runtime"
"sync/atomic"
)
func main() {
var a, b int32 = 0, 0
go func() {
a = 2
atomic.StoreInt32(&b, 1)
}()
for {
if n := atomic.LoadInt32(&b); n == 1 {
fmt.Println(a) // 2
break
}
runtime.Gosched()
}
}
請閱讀原子操作一文來獲取如何使用原子操作。
調(diào)用runtime.SetFinalizer(x, f)
發(fā)生在終結(jié)調(diào)用f(x)
被執(zhí)行之前。
更多建議: