Go:Memory Model

Go的內(nèi)存模型

看完這篇文章你會明白

  • 一個Go程序在啟動時的執(zhí)行順序
  • 并發(fā)的執(zhí)行順序
  • 并發(fā)環(huán)境下如何保證數(shù)據(jù)的同步性
  • 同步性的錯誤示范

介紹

Go內(nèi)存模型指定條件亲桦,在該條件下,可以保證一個goroutine中的變量讀取可以觀察到不同goroutine寫入同一個變量而產(chǎn)生的值

建議

在一個程序中秕豫,多個goroutine同時修改一個都要訪問的數(shù)據(jù)必須將這種訪問進行序列化(也就是說需要有一個誰先誰后的規(guī)則)陕习。為了序列化的訪問霎褐,可以使用通道操作或其他同步機制(如 syncsync/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)來做相對排序,如lockchannel 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.Mutexsync.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

以下都是“同步”用法的不正確示范

  1. 同步發(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规惰。

  1. 雙重鎖定是試圖避免同步的開銷睬塌。 例如,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的寫入并沒有同步性。

  1. 另一個錯誤的用法就是循環(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)。

  1. 上例的一個微妙變種:
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的初始化值苏研。

附:官方文檔

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市腮郊,隨后出現(xiàn)的幾起案子摹蘑,更是在濱河造成了極大的恐慌,老刑警劉巖轧飞,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件衅鹿,死亡現(xiàn)場離奇詭異,居然都是意外死亡过咬,警方通過查閱死者的電腦和手機大渤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來掸绞,“玉大人泵三,你說我怎么就攤上這事∠蔚В” “怎么了烫幕?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長敞映。 經(jīng)常有香客問我较曼,道長,這世上最難降的妖魔是什么振愿? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任捷犹,我火速辦了婚禮弛饭,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘萍歉。我一直安慰自己侣颂,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布枪孩。 她就那樣靜靜地躺著横蜒,像睡著了一般。 火紅的嫁衣襯著肌膚如雪销凑。 梳的紋絲不亂的頭發(fā)上丛晌,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天,我揣著相機與錄音斗幼,去河邊找鬼澎蛛。 笑死,一個胖子當(dāng)著我的面吹牛蜕窿,可吹牛的內(nèi)容都是我干的谋逻。 我是一名探鬼主播,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼桐经,長吁一口氣:“原來是場噩夢啊……” “哼毁兆!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起阴挣,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤气堕,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后畔咧,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體茎芭,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年誓沸,在試婚紗的時候發(fā)現(xiàn)自己被綠了梅桩。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡拜隧,死狀恐怖宿百,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情洪添,我是刑警寧澤垦页,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站薇组,受9級特大地震影響外臂,放射性物質(zhì)發(fā)生泄漏坐儿。R本人自食惡果不足惜律胀,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一宋光、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧炭菌,春花似錦罪佳、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至克握,卻和暖如春蕾管,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背菩暗。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工掰曾, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人停团。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓旷坦,卻偏偏與公主長得像,于是被迫代替她去往敵國和親佑稠。 傳聞我的和親對象是個殘疾皇子秒梅,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,792評論 2 345

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn)舌胶,斷路器捆蜀,智...
    卡卡羅2017閱讀 134,599評論 18 139
  • 本文翻譯自Sameer Ajmani的文章《Go Concurrency Patterns: Pipelines ...
    大蟒傳奇閱讀 3,858評論 0 15
  • Java NIO(New IO)是從Java 1.4版本開始引入的一個新的IO API,可以替代標(biāo)準(zhǔn)的Java I...
    JackChen1024閱讀 7,536評論 1 143
  • 介紹 如何保證在一個goroutine中看到在另一個goroutine修改的變量的值幔嫂,這篇文章進行了詳細說明漱办。 建...
    51reboot閱讀 19,626評論 11 41
  • 1 黑夜點亮了星的光 你的悲傷 點亮了我的黑暗 2 輕舟入了淺淺的港灣 那遠方 也被擱淺了 3 與你在一起 憂愁 ...
    王錯錯閱讀 804評論 4 10