Go的內存模型

轉載請註明出處,原文連接http://tailnode.tk/2017/01/Go...node

說明

翻譯自The Go Memory Modelgolang

介紹

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

建議

若是程序中修改數據時有其餘goroutine同時讀取,那麼必須將讀取串行化。爲了串行化訪問,請使用channel或其餘同步原語,例如syncsync/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的:app

  1. r不先行發生於w函數

  2. wr前沒有對v的其餘寫操做atom

爲了保證對變量v的讀操做r看到對v的寫操做w,要確保wr容許看到的惟一寫操做。即當下麪條件知足時,r 被保證看到w線程

  1. w先行發生於r翻譯

  2. 其餘對共享變量v的寫操做要麼在w前,要麼在r後。
    這一對條件比前面的條件更嚴格,須要沒有其餘寫操做與wr併發發生。code

單獨的goroutine中沒有併發,因此上面兩個定義是相同的:讀操做r看到最近一次的寫操做w寫入v的值。當多個goroutine訪問共享變量v時,它們必須使用同步事件來創建先行發生這一條件來保證讀操做能看到須要的寫操做。
對變量v的零值初始化在內存模型中表現的與寫操做相同。
對大於一個字的變量的讀寫操做表現的像以不肯定順序對多個一字大小的變量的操做。

同步

初始化

程序的初始化在單獨的goroutine中進行,但這個goroutine可能會建立出併發執行的其餘goroutine。
若是包p引入(import)包q,那麼q的init函數的結束先行發生於p的全部init函數開始
main.main函數的開始發生在全部init函數結束以後

建立goroutine

go關鍵字開啓新的goroutine,先行發生於這個goroutine開始執行,例以下面程序:

var a string

func f() {
    print(a)
}

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

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

銷燬goroutine

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

var a string

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

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

channel通訊

channel通訊是goroutine同步的主要方法。每個在特定channel的發送操做都會匹配到一般在另外一個goroutine執行的接收操做。
在channel的發送操做先行發生於對應的接收操做完成
例如:

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的寫先行發生於在c上的發送,先行發生於在c上的對應的接收完成,先行發生於print
對channel的關閉先行發生於接收到零值,由於channel已經被關閉了。
在上面的例子中,將c <- 0替換爲close(c)還會產生一樣的結果。
無緩衝channel的接收先行發生於發送完成
以下程序(和上面相似,只交換了對channel的讀寫位置並使用了非緩衝channel):

var c = make(chan int)
var a string

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

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

若是是帶緩衝的channel(例如c = make(chan int, 1)),程序不保證打印出"hello, world"(可能打印空字符,程序崩潰或其餘行爲)。
在容量爲C的channel上的第k個接收先行發生於從這個channel上的第k+C次發送完成。
這條規則將前面的規則推廣到了帶緩衝的channel上。能夠經過帶緩衝的channel來實現計數信號量:channel中的元素數量對應着活動的數量,channel的容量表示同時活動的最大數量,發送元素獲取信號量,接收元素釋放信號量,這是限制併發的一般用法。
下面程序爲work中的每一項開啓一個goroutine,但這些goroutine經過有限制的channel來確保最多同時執行三個工做函數(w)。

var 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()返回
下面程序:

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"。第一次調用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()返回。
經過once.Do(f)執行f()先行發生(指f()返回)於其餘的once.Do(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()
}

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

錯誤的同步方法

注意,讀操做r可能會看到併發的寫操做w。即便這樣也不能代表r以後的讀能看到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。
這個事實證實一些舊的習慣是錯誤的。
雙重檢查鎖定是爲了不同步的資源消耗。例如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賦值。此程序可能會錯誤地輸出空字符而不是"hello, world"。
另外一個錯誤的習慣是忙等待 例如:

var a string
var done bool

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

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

和以前程序相似,在main中看到done被賦值不能保證看到a被賦值,因此此程序也可能打印出空字符。更糟糕的是由於兩個線程間沒有同步事件,在main中可能永遠不會看到done被賦值,因此main中的循環不保證能結束。
對程序作一個微小的改變:

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而且退出了循環,也不能保證看到g.msg的初始化值。 在上面全部的例子中,解決辦法都是相同的:明確的使用同步。

相關文章
相關標籤/搜索