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

1 簡介

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

2 建議

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

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

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

不要自作聰明。

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

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

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

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

對變量 v 的讀操作 r 被允許觀測到對 v 的寫操作 w 當以下條件同時滿足時:

  1. r 沒有先行發(fā)生于 w。
  2. 沒有有另一個對 v 的 寫操作 w'w 之后, r 之前發(fā)生。

為了保證 對變量 v 的讀操作 r 能夠觀測到某個對 v 的寫操作 w,要確保 wr 被允許觀測到的唯一的寫操作。這就是說,確保 r 觀測到 w 當同時滿足下列條件:

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

這對條件的要求要強于第一對條件;它約束了沒有其他的寫操作和 wr 同時發(fā)生。

在一個 goroutine 內(nèi),沒有并發(fā),因此兩個定義是等價的:讀操作 r 觀測到的值是最近的對 v 的寫操作 w 寫入的。當多個 goroutine 同時訪問一個共享變量 v 時,他們必須使用同步事件建立先行發(fā)生(happens-before)條件確保讀取期望的寫入值。

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

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

4 同步

4.1 初始化

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

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

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

4.2 Goroutine 創(chuàng)建

go 語句啟動了一個新的 goroutine, 先行發(fā)生于 goroutine 的開始執(zhí)行。
例如,對這個程序:

var a string

func f() {
    print(a)
}

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

調(diào)用 hello 將打印 hello, world 在未來某個時點(或許在 hello 函數(shù)返回之后)

4.3 Goroutine 銷毀

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

var a string

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

a 的賦值操作并為被任何同步事件所保證,因此不保證任何其他的 goroutine 能夠觀測到該賦值。事實上,一個激進的編譯器或許或刪除整個 go 語句。

如果一個 goroutine 的影響必須被另一個 goroutine 觀測到,就得使用例如一個鎖或通道通信的同步機制建立相對順序。

4.4 通道通信

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

一個通道上的發(fā)送操作在該通道上的接收操作完成之前發(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ā)送操作,在 c 上的發(fā)送在相關(guān)的接收操作完成之前發(fā)生,在 c 上的接收完成先行發(fā)生于 print 函數(shù)。

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

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

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

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

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 的接收操作之前發(fā)生,對通道 c 的接收操作在相關(guān)的發(fā)送操作完成之前發(fā)生,對通道 c 的發(fā)送完成在 print 函數(shù)之前發(fā)生。

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

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

這個規(guī)則泛化了之前對于帶緩沖通道的規(guī)則。它允許計數(shù)信號量由帶緩沖通道建模: 通道中的條目數(shù)對應(yīng)于活躍使用數(shù),通道容量對應(yīng)于最大的同時使用數(shù),發(fā)送一個條目獲取信號量,接收一個條目釋放信號量。這是限制并發(fā)的常用習慣用法。

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

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。

對于任何 sync.Mutex 或 sync.RWMutex 變量 l 和 n < m,對 l.Unlock() 的調(diào)用 n 在對 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。對 l.Unlock() 的第一次調(diào)用 (在 f 中) 在對 l.Lock() 的第二次調(diào)用(在 main 中)返回之前發(fā)生,而對 l.Lock() 的第二次調(diào)用在 print 函數(shù)之前發(fā)生。

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

4.6 Once

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

通過 once.Do(f) 對 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 會造成 hello, world 被打印兩次。第一次對 doprint 的調(diào)用會運行 setup 一次。

5 不正確的同步

注意到讀操作 r 可能觀測到和 r 同時(并發(fā))發(fā)生的寫操作 w 寫入的值。即使這種情況發(fā)生,但并不意味著任何發(fā)生在 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 函數(shù)中,觀測到 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 的寫入。因此,這個程序也可能打印出空字符串。更糟糕的事,無法保證對 done 的寫入會被 main 函數(shù)觀測到,因為在兩個線程之間沒有同步機制。在 main 中的循環(huán)不保證能夠結(jié)束。

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

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

在上述所有的例子中,解決方案只有一個:使用顯式同步機制。

6 參考資料

  1. https://golang.org/ref/mem
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

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

友情鏈接更多精彩內(nèi)容