Go的記憶體模型

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
Golang語言

介紹

如何保證在一個goroutine中看到在另一個goroutine修改的變數的值,這篇文章進行了詳細說明。

建議

如果程式中修改資料時有其他goroutine同時讀取,那麼必須將讀取序列化。為了序列化訪問,請使用channel或其他同步原語,例如sync和sync/atomic來保護資料。

先行發生

在一個gouroutine中,讀和寫一定是按照程式中的順序執行的。即編譯器和處理器只有在不會改變這個goroutine的行為時才可能修改讀和寫的執行順序。由於重排,不同的goroutine可能會看到不同的執行順序。例如,一個goroutine執行a = 1;b = 2;,另一個goroutine可能看到ba之前更新。

為了說明讀和寫的必要條件,我們定義了先行發生(Happens Before)--Go程式中執行記憶體操作的偏序。如果事件e1發生在e2前,我們可以說e2發生在e1後。如果e1不發生在e2前也不發生在e2後,我們就說e1e2是並發的。

在單獨的goroutine中先行發生的順序即是程式中表達的順序。
當下麵條件滿足時,對變數v的讀操作r被允許看到對v的寫操作w的:

1 r不先行發生於w
2 在w後r前沒有對v的其他寫操作

為了保證對變數v的讀操作r看到對v的寫操作w,要確保wr允許看到的唯一寫操作。即當下麵條件滿足時,r 被保證看到w

1 w先行發生於r
2 其他對共用變數v的寫操作要麼在w前,要麼在r後。

這一對條件比前面的條件更嚴格,需要沒有其他寫操作與w或r並發發生。

單獨的goroutine中沒有並發,所以上面兩個定義是相同的:讀操作r看到最近一次的寫操作w寫入v的值。當多個goroutine訪問共用變數v時,它們必須使用同步事件來建立先行發生這一條件來保證讀操作能看到需要的寫操作。 對變數v的零值初始化在記憶體模型中表現的與寫操作相同。 對大於一個字的變數的讀寫動作表現的像以不確定順序對多個一字大小的變數的操作。

同步

初始化

程式的初始化在單獨的goroutine中進行,但這個goroutine可能會建立出並發執行的其他goroutine。

如果包p引入(import)包q,那麼q的init函數的結束先行發生於p的所有init函數開始 main.main函數的開始發生在所有init函數結束之後

建立goroutine

go關鍵字開啟新的goroutine,先行發生於這個goroutine開始執行,例如下面程式:

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

調用hello會在之後的某時刻列印出"hello, world"(可能在hello返回之後)

銷毀goroutine

gouroutine的退出並不會保證先行發生於程式的任何事件。例如下面程式:

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

沒有用任何同步操作限制對a的賦值,所以並不能保證其他goroutine能看到a的變化。實際上,一個激進的編譯器可能會刪掉整個go語句。 如果想要在一個goroutine中看到另一個goroutine的執行效果,請使用鎖或者channel這種同步機制來建立程式執行的相對順序。

channel通訊

channel通訊是goroutine同步的主要方法。每一個在特定channel的發送操作都會匹配到通常在另一個goroutine執行的接收操作。

在channel的發送操作先行發生於對應的接收操作完成 例如:

c = make(chan int, 10)var a stringfunc f() {    a = "hello, world"    c <- 0}func main() {    go f()    <-c        print(a)}

這個程式能保證列印出"hello, world"。對a的寫先行發生於在c上的發送,先行發生於在c上的對應的接收完成,先行發生於print

對channel的關閉先行發生於接收到零值,因為channel已經被關閉了

在上面的例子中,將c <- 0替換為close(c)還會產生同樣的結果。

無緩衝channel的接收先行發生於發送完成

如下程式(和上面類似,只交換了對channel的讀寫位置並使用了非緩衝channel):

 c = make(chan int)var a stringfunc f() {    a = "hello, world"    <-c}
n() {    go f()    c <- 0    print(a)}

此程式也能保證列印出"hello, world"。對a的寫先行發生於從c接收,先行發生於向c發送完成,先行發生於print

如果是帶緩衝的channel(例如c = make(chan int, 1)),程式不保證列印出"hello, world"(可能列印Null 字元,程式崩潰或其他行為)。

在容量為C的channel上的第k個接收先行發生於從這個channel上的第k+C次發送完成

這條規則將前面的規則推廣到了帶緩衝的channel上。可以通過帶緩衝的channel來實現計數訊號量:channel中的元素數量對應著活動的數量,channel的容量表示同時活動的最大數量,發送元素擷取訊號量,接收元素釋放訊號量,這是限制並發的通常用法。

下面程式為work中的每一項開啟一個goroutine,但這些goroutine通過有限制的channel來確保最多同時執行三個工作函數(w)。

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

sync包實現了兩個鎖的資料類型sync.Mutex和sync.RWMutex。

對任意的sync.Mutex或sync.RWMutex變數l和n < m,n次調用l.Unlock()先行發生於m次l.Lock()返回
下面程式:

l sync.Mutexvar a stringfunc f() {    a = "hello, world"    l.Unlock()}func main() {    l.Lock()    go f()    l.Lock()    print(a)}

能保證列印出"hello, world"。第一次調用l.Unlock()(在f()中)先行發生於main中的第二次l.Lock()返回, 先行發生於print。

對於sync.RWMutex變數l,任意的函數調用l.RLock滿足第n次l.RLock後發生於第n次調用l.Unlock,對應的l.RUnlock先行發生於第n+1次調用l.Lock。

Once

sync包的Once為多個goroutine提供了安全的初始化機制。能在多個線程中執行once.Do(f),但只有一個f()會執行,其他調用會一直阻塞直到f()返回。
通過執行先行發生(指f()返回)於其他的返回。
如下程式:

a stringvar once sync.Oncefunc setup() {    a = "hello, world"}func doprint() {    once.Do(setup)    print(a)}func twoprint() {    go doprint()        go doprint()}

調用twoprint會列印"hello, world"兩次。setup只在第一次doprint時執行。

錯誤的同步方法

注意,讀操作r可能會看到並發的寫操作w。即使這樣也不能表明r之後的讀能看到w之前的寫。
如下程式:

 a, b intfunc f() {    a = 1    b = 2}func g() {    print(b)        print(a)}    func main() {    go f()    g()}

g可能先列印出2然後是0。
這個事實證明一些舊的習慣是錯誤的。
雙重檢查鎖定是為了避免同步的資源消耗。例如twoprint程式可能會錯誤的寫成:

 a stringvar done boolfunc setup() {    a = "hello, world"    done = true}func doprint() {    if !done {        once.Do(setup)    }        print(a)}func twoprint() {    go doprint()        go doprint()}

doprint中看到done被賦值並不保證能看到對a賦值。此程式可能會錯誤地輸出Null 字元而不是"hello, world"。
另一個錯誤的習慣是忙等待 例如:

a stringvar done boolfunc setup() {    a = "hello, world"    done = true}func main() {    go setup()        for !done {    }        print(a)}

和之前程式類似,在main中看到done被賦值不能保證看到a被賦值,所以此程式也可能列印出Null 字元。更糟糕的是因為兩個線程間沒有同步事件,在main中可能永遠不會看到done被賦值,所以main中的迴圈不保證能結束。
對程式做一個微小的改變:

T struct {    msg string}var g *Tfunc setup() {    t := new(T)    t.msg = "hello, world"    g = t}func main() {    go setup()        for g == nil {    }        print(g.msg)}

即使main看到了g != nil並且退出了迴圈,也不能保證看到g.msg的初始化值。
在上面所有的例子中,解決辦法都是相同的:明確的使用同步。

作者丨 tailnode
連結丨 http://tailnode.tk/2017/01/Go的記憶體模型/
原文丨https://golang.org/ref/mem

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.