Go 內(nèi)存模型 (2014年5月31日版本)

1 簡(jiǎn)介

Go 內(nèi)存模型指定了一個(gè)條件,在該條件下奕坟,在一個(gè) goroutine 中一個(gè)變量的讀取可保證能夠觀測(cè)到被其他 goroutine 對(duì)該變量寫入的變化值。

2 建議

修改能夠被多個(gè) goroutine 同時(shí)訪問(wèn)到的數(shù)據(jù)的程序必須序列化此過(guò)程。

為了序列化這個(gè)訪問(wèn)過(guò)程,使用通道操作或其他例如在 syncsync/atomic 包中的同步原語(yǔ)保護(hù)數(shù)據(jù)骏掀。

如果你一定要閱讀該文檔的剩余部分以理解程序的行為,那么你就太聰明了。

不要自作聰明截驮。

3 先行發(fā)生原則(Happens Before)

在一個(gè) goroutine 中笑陈,讀寫一定會(huì)以在程序中的指定順序而執(zhí)行。這意味著葵袭,編譯器和處理器可能會(huì)重新排序在一個(gè) goroutine 中的讀寫執(zhí)行涵妥,但只有當(dāng)重排序不會(huì)改變語(yǔ)言規(guī)范定義的單一 goroutine 內(nèi)的行為表現(xiàn)時(shí)才會(huì)發(fā)生。由于這種重排序的發(fā)生坡锡,一個(gè) goroutine 中觀測(cè)到的執(zhí)行順序可能不同于另一個(gè) goroutine 中的觀察蓬网。例如,如果一個(gè) goroutine 執(zhí)行 a = 1; b=2;, 另一個(gè)可能會(huì)觀測(cè)到 b 先更新而 a 后更新鹉勒。

為了滿足讀寫需求帆锋,我們定義了 happens before 原則, 這是一種在 Go 程序中描述執(zhí)行內(nèi)存操作的偏序關(guān)系贸弥。如果事件 e1 先行發(fā)生于事件 e2, 那么我們說(shuō) e2 后發(fā)生于 e1窟坐。同理海渊,如果 e1 沒(méi)有(一定要)先行發(fā)生于 e2, 那么我們說(shuō)e1e2 同步發(fā)生绵疲。

在一個(gè) goroutine 內(nèi),happens-before 順序由程序表述臣疑。

對(duì)變量 v 的讀操作 r 被允許觀測(cè)到對(duì) v 的寫操作 w 當(dāng)以下條件同時(shí)滿足時(shí):

  1. r 沒(méi)有先行發(fā)生于 w盔憨。
  2. 沒(méi)有有另一個(gè)對(duì) v 的 寫操作 w'w 之后, r 之前發(fā)生讯沈。

為了保證 對(duì)變量 v 的讀操作 r 能夠觀測(cè)到某個(gè)對(duì) v 的寫操作 w郁岩,要確保 wr 被允許觀測(cè)到的唯一的寫操作。這就是說(shuō)缺狠,確保 r 觀測(cè)到 w 當(dāng)同時(shí)滿足下列條件:

  1. w 先行發(fā)生于 r问慎。
  2. 任何其他對(duì)共享變量 v 的寫操作要么在 w 之前發(fā)生,要么在 r 之后發(fā)生挤茄。

這對(duì)條件的要求要強(qiáng)于第一對(duì)條件如叼;它約束了沒(méi)有其他的寫操作和 wr 同時(shí)發(fā)生。

在一個(gè) goroutine 內(nèi)穷劈,沒(méi)有并發(fā)笼恰,因此兩個(gè)定義是等價(jià)的:讀操作 r 觀測(cè)到的值是最近的對(duì) v 的寫操作 w 寫入的。當(dāng)多個(gè) goroutine 同時(shí)訪問(wèn)一個(gè)共享變量 v 時(shí)歇终,他們必須使用同步事件建立先行發(fā)生(happens-before)條件確保讀取期望的寫入值社证。

在內(nèi)存模型中對(duì)變量 v 的初始化含類型零值的操作其表現(xiàn)與寫操作一致。

讀取和寫入超過(guò)一個(gè)機(jī)器字的值其表現(xiàn)與以非指定順序進(jìn)行多個(gè)機(jī)器字操作一致评凝。

4 同步

4.1 初始化

程序初始化運(yùn)行在一個(gè) goroutine 內(nèi)追葡,但是這個(gè) goroutine 可能創(chuàng)建其他 goroutines,客觀產(chǎn)生并發(fā)運(yùn)行的效果。

如果包 p 引入了包 q, 對(duì) q 的 init 函數(shù)的完成要先行發(fā)生于任何對(duì) p 的函數(shù)的開(kāi)始宜肉。

函數(shù) main.main 的開(kāi)始要在所有 init 函數(shù)的完成后發(fā)生疾渣。

4.2 Goroutine 創(chuàng)建

go 語(yǔ)句啟動(dòng)了一個(gè)新的 goroutine, 先行發(fā)生于 goroutine 的開(kāi)始執(zhí)行崖飘。
例如榴捡,對(duì)這個(gè)程序:

var a string

func f() {
    print(a)
}

func hello() {
    a = "hello, world"
    go f()
}

調(diào)用 hello 將打印 hello, world 在未來(lái)某個(gè)時(shí)點(diǎn)(或許在 hello 函數(shù)返回之后)

4.3 Goroutine 銷毀

不保證一個(gè) goroutine 的退出先行發(fā)生于程序的任何事件。例如朱浴,對(duì)這個(gè)程序:

var a string

func hello() {
    go func() { a = "hello" }()
    print(a)
}

對(duì) a 的賦值操作并為被任何同步事件所保證吊圾,因此不保證任何其他的 goroutine 能夠觀測(cè)到該賦值。事實(shí)上翰蠢,一個(gè)激進(jìn)的編譯器或許或刪除整個(gè) go 語(yǔ)句项乒。

如果一個(gè) goroutine 的影響必須被另一個(gè) goroutine 觀測(cè)到,就得使用例如一個(gè)鎖或通道通信的同步機(jī)制建立相對(duì)順序梁沧。

4.4 通道通信

通道通信是 goroutines 間同步的主要方法檀何。每個(gè)特定通道上的發(fā)送操作要與該通道上的接收操作對(duì)應(yīng),通常用于不同的 goroutine廷支。

一個(gè)通道上的發(fā)送操作在該通道上的接收操作完成之前發(fā)生频鉴。

這個(gè)程序:

var c = make(chan int, 10)
var a string

func f() {
    a = "hello, world"
    c <- 0
}

func main() {
    go f()
    <-c
    print(a)
}

這個(gè)程序能保證打印出 hello, world。對(duì) a 的寫操作先行發(fā)生于在通道 c 上的發(fā)送操作恋拍,在 c 上的發(fā)送在相關(guān)的接收操作完成之前發(fā)生垛孔,在 c 上的接收完成先行發(fā)生于 print 函數(shù)。

一個(gè)通道上的關(guān)閉操作在由于通道被關(guān)閉的原因接收到返回的零值之前發(fā)生施敢。

在前面的例子里周荐,用 close(c) 代替 c <- 0 會(huì)使程序表現(xiàn)同樣的行為。

一個(gè)非緩沖通道上的接收操作在該通道上的發(fā)送操作完成之前發(fā)生僵娃。

以下程序(如同上面的程序概作,但是交換了發(fā)送和接收語(yǔ)句蘸嘶,使用了非緩沖通道):

var c = make(chan int)
var a string

func f() {
    a = "hello, world"
    <-c
}
func main() {
    go f()
    c <- 0
    print(a)
}

該程序也能保證打印出 hello, world象颖。對(duì) a 的寫操作在對(duì)通道 c 的接收操作之前發(fā)生,對(duì)通道 c 的接收操作在相關(guān)的發(fā)送操作完成之前發(fā)生练链,對(duì)通道 c 的發(fā)送完成在 print 函數(shù)之前發(fā)生先壕。

如果通道是緩沖的(例如瘩扼,c = make(chan int, 1)),那么程序?qū)⒉荒鼙WC打印出 hello, world垃僚。(可能會(huì)打印出空字符串集绰,崩潰或產(chǎn)生其他什么效果。)

容量為 C 的通道上第 k 個(gè)接收操作在該通道第 k + C 個(gè)發(fā)送操作完成之前發(fā)生谆棺。

這個(gè)規(guī)則泛化了之前對(duì)于帶緩沖通道的規(guī)則栽燕。它允許計(jì)數(shù)信號(hào)量由帶緩沖通道建模: 通道中的條目數(shù)對(duì)應(yīng)于活躍使用數(shù)罕袋,通道容量對(duì)應(yīng)于最大的同時(shí)使用數(shù),發(fā)送一個(gè)條目獲取信號(hào)量碍岔,接收一個(gè)條目釋放信號(hào)量浴讯。這是限制并發(fā)的常用習(xí)慣用法。

以下程序在工作列表中每次進(jìn)入都會(huì)啟動(dòng)一個(gè) goroutine, 但是 goroutines 使用 limit 通道進(jìn)行協(xié)調(diào)以確保至多只有3個(gè)工作函數(shù)同時(shí)運(yùn)行蔼啦。

var limit = make(chan int, 3)

func main() {
    for _, w := range work {
        go func(w func()) {
            limit <- 1
            w()
            <-limit
        }(w)
    }
    select{}
}

4.5 鎖

sync 包引入了兩種鎖類型, sync.Mutexsync.RWMutex榆纽。

對(duì)于任何 sync.Mutex 或 sync.RWMutex 變量 l 和 n < m,對(duì) l.Unlock() 的調(diào)用 n 在對(duì) l.Lock() 的調(diào)用 m 返回之前發(fā)生捏肢。

以下程序:

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奈籽。對(duì) l.Unlock() 的第一次調(diào)用 (在 f 中) 在對(duì) l.Lock() 的第二次調(diào)用(在 main 中)返回之前發(fā)生,而對(duì) l.Lock() 的第二次調(diào)用在 print 函數(shù)之前發(fā)生鸵赫。

對(duì)于任何對(duì) sync.RWMutext 變量 ll.RLock() 調(diào)用衣屏,存在一個(gè)這樣的調(diào)用 n, 其 l.RLockl.Unlock 的調(diào)用 n 之后發(fā)生(返回),匹配的 l.RUnlockl.Lock 的調(diào)用 n + 1 前發(fā)生辩棒。

4.6 Once

sync 包提供了一種機(jī)制狼忱,該機(jī)制允許多個(gè) goroutines 使用 Once 類型進(jìn)行安全的初始化。多個(gè)線程對(duì)一個(gè)特定的函數(shù) f 能都執(zhí)行 once.Do(f)一睁, 但是只有一個(gè)會(huì)運(yùn)行 f(), 其他調(diào)用會(huì)阻塞直到 f() 返回钻弄。

通過(guò) once.Do(f) 對(duì) f() 的調(diào)用在任何調(diào)用 once.Do(f) 返回之前發(fā)生(返回)。

在以下程序中:

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()
}

調(diào)用 twoprint 會(huì)造成 hello, world 被打印兩次卖局。第一次對(duì) doprint 的調(diào)用會(huì)運(yùn)行 setup 一次斧蜕。

5 不正確的同步

注意到讀操作 r 可能觀測(cè)到和 r 同時(shí)(并發(fā))發(fā)生的寫操作 w 寫入的值。即使這種情況發(fā)生砚偶,但并不意味著任何發(fā)生在 r 之后的讀操作能夠觀測(cè)到 w 之前的寫操作寫入的值。
在以下程序中:

var a, b int

func f() {
    a = 1
    b = 2
}

func g() {
    print(b)
    print(a)
}

func main() {
    go f()
    g()
}

有可能 g 打印出 2 和 0洒闸。

這個(gè)事實(shí)會(huì)使一些常見(jiàn)用法無(wú)效染坯。

雙重檢查鎖是一種避免同步開(kāi)銷的方法。例如丘逸,twoprint 程序可能會(huì)被不正確地寫為:

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 函數(shù)中单鹿,觀測(cè)到 done 的寫入意味著觀測(cè)到 a 的寫入值。這個(gè)版本可能會(huì)(不正確地)打印出一個(gè)空字符串深纲,而不是 hello, world仲锄。

另一個(gè)不正確的習(xí)慣用法是忙于等待一個(gè)值,例如:

var a string
var done bool

func setup() {
    a = "hello, world"
    done = true
}

func main() {
    go setup()
    for !done {
    }
    print(a)
}

正如之前的程序湃鹊,無(wú)法保證在 main 中儒喊,觀測(cè)到對(duì) done 的寫入意味著觀測(cè)到對(duì) a 的寫入。因此币呵,這個(gè)程序也可能打印出空字符串怀愧。更糟糕的事,無(wú)法保證對(duì) done 的寫入會(huì)被 main 函數(shù)觀測(cè)到,因?yàn)樵趦蓚€(gè)線程之間沒(méi)有同步機(jī)制芯义。在 main 中的循環(huán)不保證能夠結(jié)束哈垢。

又一個(gè)這種模式的微妙變體,例如如下程序:

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 函數(shù)觀測(cè)到 g != nil 并且退出了循環(huán)扛拨,仍然無(wú)法保證它能觀測(cè)到 g.msg 的初始化值耘分。

在上述所有的例子中,解決方案只有一個(gè):使用顯式同步機(jī)制绑警。

6 參考資料

  1. https://golang.org/ref/mem
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末陶贼,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子待秃,更是在濱河造成了極大的恐慌拜秧,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,427評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件章郁,死亡現(xiàn)場(chǎng)離奇詭異枉氮,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)暖庄,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門聊替,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人培廓,你說(shuō)我怎么就攤上這事惹悄。” “怎么了肩钠?”我有些...
    開(kāi)封第一講書人閱讀 165,747評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵泣港,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我价匠,道長(zhǎng)当纱,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 58,939評(píng)論 1 295
  • 正文 為了忘掉前任踩窖,我火速辦了婚禮坡氯,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘洋腮。我一直安慰自己箫柳,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,955評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布啥供。 她就那樣靜靜地躺著悯恍,像睡著了一般。 火紅的嫁衣襯著肌膚如雪滤灯。 梳的紋絲不亂的頭發(fā)上坪稽,一...
    開(kāi)封第一講書人閱讀 51,737評(píng)論 1 305
  • 那天曼玩,我揣著相機(jī)與錄音,去河邊找鬼窒百。 笑死黍判,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的篙梢。 我是一名探鬼主播顷帖,決...
    沈念sama閱讀 40,448評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼渤滞!你這毒婦竟也來(lái)了贬墩?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 39,352評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤妄呕,失蹤者是張志新(化名)和其女友劉穎陶舞,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體绪励,經(jīng)...
    沈念sama閱讀 45,834評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡肿孵,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,992評(píng)論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了疏魏。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片停做。...
    茶點(diǎn)故事閱讀 40,133評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖大莫,靈堂內(nèi)的尸體忽然破棺而出蛉腌,到底是詐尸還是另有隱情,我是刑警寧澤只厘,帶...
    沈念sama閱讀 35,815評(píng)論 5 346
  • 正文 年R本政府宣布烙丛,位于F島的核電站,受9級(jí)特大地震影響懈凹,放射性物質(zhì)發(fā)生泄漏蜀变。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,477評(píng)論 3 331
  • 文/蒙蒙 一介评、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧爬舰,春花似錦们陆、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 32,022評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至垃你,卻和暖如春椅文,著一層夾襖步出監(jiān)牢的瞬間喂很,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,147評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工皆刺, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留少辣,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,398評(píng)論 3 373
  • 正文 我出身青樓羡蛾,卻偏偏與公主長(zhǎng)得像漓帅,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子痴怨,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,077評(píng)論 2 355

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

  • 介紹 如何保證在一個(gè)goroutine中看到在另一個(gè)goroutine修改的變量的值忙干,這篇文章進(jìn)行了詳細(xì)說(shuō)明。 建...
    51reboot閱讀 19,664評(píng)論 11 41
  • Go的內(nèi)存模型 看完這篇文章你會(huì)明白 一個(gè)Go程序在啟動(dòng)時(shí)的執(zhí)行順序 并發(fā)的執(zhí)行順序 并發(fā)環(huán)境下如何保證數(shù)據(jù)的同步...
    初級(jí)賽亞人閱讀 2,855評(píng)論 0 2
  • 并發(fā)(并行)浪藻,一直以來(lái)都是一個(gè)編程語(yǔ)言里的核心主題之一捐迫,也是被開(kāi)發(fā)者關(guān)注最多的話題;Go語(yǔ)言作為一個(gè)出道以來(lái)就自帶...
    駐馬聽(tīng)雪閱讀 3,003評(píng)論 3 27
  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理爱葵,服務(wù)發(fā)現(xiàn)施戴,斷路器,智...
    卡卡羅2017閱讀 134,672評(píng)論 18 139
  • Chapter 8 Goroutines and Channels Go enable two styles of...
    SongLiang閱讀 1,589評(píng)論 0 3