Go的內(nèi)存模型
看完這篇文章你會明白
- 一個Go程序在啟動時的執(zhí)行順序
- 并發(fā)的執(zhí)行順序
- 并發(fā)環(huán)境下如何保證數(shù)據(jù)的同步性
- 同步性的錯誤示范
介紹
Go內(nèi)存模型指定條件亲桦,在該條件下,可以保證一個goroutine中的變量讀取可以觀察到不同goroutine寫入同一個變量而產(chǎn)生的值
建議
在一個程序中秕豫,多個goroutine同時修改一個都要訪問的數(shù)據(jù)必須將這種訪問進行序列化(也就是說需要有一個誰先誰后的規(guī)則)陕习。為了序列化的訪問霎褐,可以使用通道操作或其他同步機制(如 sync 和 sync/atomic包中的方法)保護數(shù)據(jù)。
如果你想詳細了解一個程序的行為该镣,請繼續(xù)往下讀冻璃。
Happens Before原則
在單個goroutine中,讀取和寫入必須按照程序指定的順序執(zhí)行。 也就是說省艳,編譯器和處理器可以重新排序在單個goroutine中執(zhí)行的讀取和寫入娘纷。 如果有兩個goroutine時,一個goroutine觀察到的執(zhí)行順序可能不同于另一個goroutine定義的順序跋炕。 例如赖晶,如果一個goroutine執(zhí)行a= 1; b = 2;另一個可能會在觀察到a值更新之前先觀察b的更新值。
為了指定讀和寫的順序辐烂,我們定義了Happens Before原則遏插,它表示一個Go程序中執(zhí)行內(nèi)存操作的部分順序。
- 如果事件e1發(fā)生在e2之前纠修,那么我們就可以說事件e2發(fā)生在e1之后
- 如果e1既不發(fā)生在e2之前胳嘲,也不發(fā)生在e2之后,那么我們就說e1和e2是同時發(fā)生的
在單goroutine的程序中扣草,Happens-Before的順序就是程序中表達的順序了牛。
*注:這里的單goroutine的程序是指程序中沒有使用go關(guān)鍵字聲明一個goroutine的操作。
注:任何一個Go程序中都不會只有一個goroutine的存在辰妙,即使你沒有顯示聲明過(go關(guān)鍵字聲明)鹰祸,程序在啟動時除了有一個main的goroutine存在之外,至少還會隱式的創(chuàng)建一個goroutine用于gc密浑,使用runtime.NumGoroutine()可以得到程序中的goroutine的數(shù)量
對一個變量v的寫操作(w)會影響到對v的讀操作(r)蛙婴,那么:
- 這個讀操作(r)發(fā)生在寫操作(w)之后
- 沒有其他的寫操作(w')發(fā)生在寫操作(w)和讀操作(r)之間
為了保證對變量v的一個特定讀操作(r)讀取到一個特定寫操作(w)寫入的特定值,確保w是唯一的一個寫操作肴掷,那么:
- w發(fā)生在r之前
- 對共享變量v的任何其他寫入都發(fā)生在w之前或之后
這個條件相對于第一個更加苛刻敬锐,它需要保證沒有其他的寫操作發(fā)生在w和r之間
在一個goroutine中背传,這兩個定義是等價的呆瞻,因為它沒并發(fā)性可言;但是當(dāng)多個goroutine同時訪問變量v時径玖,我們必須使用同步事件(synchronization events)來滿足Happends-Before條件以確保讀操作(r)觀察到期望的寫操作(w)的值痴脾。
同步(Synchronization)
初始化(Initialization)
程序初始化在單個goroutine中運行,但是goroutine可能會創(chuàng)建其他同時運行的goroutine
如:在package p中import package q梳星,那么q的init()函數(shù)先于p的init()函數(shù)赞赖,起始函數(shù)main.main()發(fā)生在所有包的init()函數(shù)之后
Goroutine creation
go關(guān)鍵詞聲明一個goroutine的動作發(fā)生在goroutine(調(diào)用go的那個goroutine)執(zhí)行之前
var a string
func f() {
print(a)
}
func hello() {
a = "hello, world"
go f()
}
func main(){
hello()
}
結(jié)果
- 打印hello,world,說明f()所在的goroutine執(zhí)行了
- 什么都不會打印冤灾,說明f()所在的goroutine沒執(zhí)行前域,并不代表f()這個goroutine沒被加入到goroutine的執(zhí)行隊列中去,只是f()沒來得及執(zhí)行韵吨,而程序已經(jīng)退出了匿垄,(Go會將所有的goroutine加入到一個待執(zhí)行的隊列中),之后它會被gc回收處理。
Goroutine destruction
一個goroutine退出時椿疗,并不能保證它一定發(fā)生在程序的某個事件之前
var a string
func hello() {
go func() { a = "hello" }()
print(a)
}
在這個匿名goroutine退出時漏峰,并不能確保它發(fā)生在事件print(a)之前,因為沒有同步事件(synchronization events)跟隨變量a分配届榄,所以并不能保證a的修改能被其他goroutine觀察到浅乔。事實上,約束性強一點的編譯器還可能會在你保存時刪除go聲明铝条。
一個goroutine對a的修改想要其他的goroutine能觀察到靖苇,可以使用同步機制(synchronization mechanism)來做相對排序,如lock和channel communication
Channel communication
Channel是引用類型攻晒,它的底層數(shù)據(jù)結(jié)構(gòu)是一個循環(huán)隊列
Channel communication是多個goroutine之間保持同步的主要方法顾复。每個發(fā)送的特定Channel與該Channel的相應(yīng)接收匹配,發(fā)送操作和接收操作通常在不同的goroutine中鲁捏。
緩沖通道(buffered channel)的Happens Before原則
- 發(fā)送操作會使通道復(fù)制被發(fā)送的元素芯砸。若因通道的緩沖空間已滿而無法立即復(fù)制,則阻塞進行發(fā)送操作的goroutine给梅。復(fù)制的目的地址有兩種假丧。當(dāng)通道已空且有接收方在等待元素值時,它會是最早等待的那個接收方持有的內(nèi)存地址动羽,否則會是通道持有的緩沖中的內(nèi)存地址包帚。
- 接收操作會使通道給出一個已發(fā)給它的元素值的副本,若因通道的緩沖空間已空而無法立即給出运吓,則阻塞進行接收操作的goroutine渴邦。一般情況下,接收方會從通道持有的緩沖中得到元素值拘哨。
- 對于同一個元素值來說谋梭,把它發(fā)送給某個通道的操作,一定會在從該通道接收它的操作完成之前完成倦青。換言之瓮床,在通道完全復(fù)制一個元素值之前,任何goroutine都不可能從它哪里接收到這個元素值的副本产镐。
一個容量為C的channel的第k個接收操作先于第(k+C)個發(fā)送操作之前完成
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的寫操作發(fā)生在c的發(fā)送操作之前,它們作為一個f()整體又發(fā)生在c的接收操作完成之前(<-c)癣亚,而<-c操作發(fā)生在print(a)之前
對Channel的Close操作發(fā)生在返回零值的接收之前丑掺,因為通道已經(jīng)關(guān)閉
注:所以對Channel的Close操作一般發(fā)生在發(fā)送結(jié)束的地方,如果在接收的地方進行Close操作述雾,并不能保證發(fā)送操作不會繼續(xù)send數(shù)據(jù)街州,而對于一個Closed的Channel進行send操作會返回一個panic: send on closed channel
所以上例中蓬豁,將c<-0的操作換成close(c)也能正確輸出"hello,world"。
假如一個channel中的每個元素值都啟動一個goroutine來處理業(yè)務(wù)菇肃,那么緩沖通道還可以有效的限制啟動的goroutine數(shù)量地粪,它總是小于等于channel的capacity的值。
var limit = make(chan int, 3)
func main() {
for _, w := range work {
go func(w func()) {
limit <- 1
w()
<-limit
}(w)
}
select{}
}
非緩沖通道(unbuffered channel)的Happens Before原則
- 向非緩沖通道發(fā)送元素值的操作會被阻塞琐谤,直到至少有一個針對該通道的接收操作進行為止蟆技。該接收操作會先得到元素值的副本,然后在喚醒發(fā)送方所在的goroutine之后返回斗忌。也就是說质礼,這時的接收操作會在對應(yīng)的發(fā)送操作完成之前完成。
- 向非緩沖通道接收元素值的操作會被阻塞织阳,直到至少有一個針對該通道的發(fā)送操作進行為止眶蕉。該發(fā)送操作會直接把元素值復(fù)制給接收方,然后在喚醒接收方所在的goroutine之后返回唧躲。也就是說造挽,這時的發(fā)送操作會在對應(yīng)的接收操作完成之前完成。
下例是將上例的接收和發(fā)送操作交換了一下弄痹,并將通道設(shè)置為非緩沖通道
var c = make(chan int)
var a string
func f() {
a = "hello, world"
<-c
}
func main() {
go f()
c <- 0
print(a)
}
同樣會打印出"hello,world"饭入,如果使用緩沖通道(buffered channel),那么程序不一定會打印出"hello,world"了(可能會打印一個空字符串肛真,崩潰谐丢,或者做些其他事)。
Locks
包sync實現(xiàn)了兩種鎖數(shù)據(jù)類型:sync.Mutex和sync.RWMutex
程序:
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"蚓让,第一次調(diào)用l.Unlock()發(fā)生在第二次調(diào)用l.Lock()(main函數(shù)里面)之前乾忱,它們整體發(fā)生在print之前。
對于Lock()和Unlock()都是成對出現(xiàn)的历极,對于一個Unlocked的變量再次進行Unlock()操作窄瘟,會panic: sync: unlock of unlocked mutex;而對于已經(jīng)Lock()的變量再次進行Lock()操作是沒有任何問題的(在不同的goroutine中)执解,無非就是誰先搶占到l變量的操作權(quán)限而已寞肖。如果在同一個goroutine中對一個Locked的變量再次進行Lock()操作將會造成deadlock
Once
包sync提供了一個安全機制纲酗,通過使用Once類型可以在存在多個goroutine的情況下進行初始化衰腌,即使多個goroutine同時并發(fā),Once也只會執(zhí)行一次觅赊。Once.Do(f)右蕊,對于函數(shù)f(),只有一個goroutine能執(zhí)行f()吮螺,其他goroutine對它的調(diào)用將會被阻塞直到返回值(只有f()執(zhí)行完畢返回時Once.Do(f)才會返回饶囚,所以在f中調(diào)用Do將會造成deadlock)帕翻。
程序:
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()的調(diào)用結(jié)果是"hello,world"的打印兩次,但是setup()函數(shù)只會執(zhí)行一次萝风。
Incorrect synchronization
以下都是“同步”用法的不正確示范
- 同步發(fā)生的讀操作r能觀察到寫操作w寫入的值嘀掸。但是這并不意味著在r之后發(fā)生的讀操作能讀取到w之前發(fā)生的寫操作寫入的值。
程序:
var a, b int
func f() {
a = 1
b = 2
}
func g() {
print(b)
print(a)
}
func main() {
go f()
g()
}
可能的一種結(jié)果是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值的寫入揩晴,但是操作setup()中a和done的寫入并沒有同步性。
- 另一個錯誤的用法就是循環(huán)等待一個值
var a string
var done bool
func setup() {
a = "hello, world"
done = true
}
func main() {
go setup()
for !done {
}
print(a)
}
在main()中贪磺,通過觀察done的寫入來實現(xiàn)觀察a的寫入硫兰,所以a最終仍可能是空。更糟糕的是寒锚,沒什么同步機制確保go setup()中done的寫入值會被main()中觀察到劫映,所以可能main()永遠不會退出循環(huán)。
- 上例的一個微妙變種:
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來退出循環(huán)刹前,但是也不能保證它能觀察到g.msg的初始化值苏研。
附:官方文檔