Go的內存模型

這篇文章主要是爲了閱讀一篇 go 的文檔,解釋了什麼狀況下一個 goroutine 對變量寫的值能被另外一個 goroutine 可靠觀察到,主要以意譯爲主,括號內爲我的理解。html

不管是用單個通道來守護併發數據的實現仍是使用 sync 和 sync/atomic 中的同步原語的實現,程序中多個 goroutine 併發操做相同數據時必定是串行的。(兩種常見的併發模型: 使用專門 channel 來操做併發訪問的數據,其它 goroutine 把本身的操做請求發給這個 channel;多個 goroutine 搶鎖來操做數據)golang

在單個 goroutine 中,對一個變量讀寫操做的真正執行順序必需要和代碼中的順序具備相同的執行效果,就是說,編譯器和處理器可能會對單個 goroutine 內的一些讀寫操做進行從新排序,但調整順序先後的執行結果要跟按代碼中的順序執行結果一致。但這種從新排序不會考慮多個 goroutine 的狀況,一個 goroutine 中的代碼所展現的執行順序和其它 goroutine 實際觀察 ( 好比另外一個 goroutine 監聽這個 goroutine 中某些變量的變化 ) 到的這個 goroutine 的執行順序可能會不一樣。好比一個 goroutine 代碼中是依次執行 a=1 和 b=2,另外一個 goroutine 可能會觀察到 b 先被賦值爲 2,而後再是 a=1。併發

Happens Before app

爲了說清楚讀和寫的請求,先定義一下這個 happens before : 用來表達 Go 程序在內存中的一小段執行順序,當咱們說 e1 在 e2 以前發生時, 就是在說 e2 在 e1 以後發生。當 e1 沒有發生在 e2 以前,並且 e2 沒有發生在 e1 以後時,咱們說 e1 和 e2 這時是併發的 ( 即咱們沒法可靠判斷 e1 和 e2 的執行順序 ) 。函數

在單個goroutine內, happens before 的順序是由代碼表達的順序決定的this

對於變量 v 的一個讀 r ,若是不可靠觀察到( 相對於可靠觀察到 ) 寫 w 對 v 的操做,那麼 r 和 w 要知足:atom

  1. r 不能發生在 w 以前(即 r,w 要麼併發發生,要麼 r 在 w 以後發生)
  2. 在 w 以後且在 r 以前沒有其餘的對 v 的寫 ( 即其它的寫要麼與 w 併發發生,要麼與 r 併發發生,要麼發生在 w 以前,要麼發生在 r 以後 )

(能夠看到,上面約束中併發就是不可靠的來源)而若是爲了保證對 v 的讀 r 可以觀察到對 v 特定的一次寫 w ,就是說要 r 僅觀察到這特定的一次 w , 爲了實現 r 可以可靠觀察到此次 w ,那要知足:spa

  1. w 發生在 r 以前 ( 相比前文兩條約束排除了 r,w 併發發生 )
  2. 其它任何對共享變量 v 的寫,要麼發生在 w 以前,要麼發生在 r 以後 ( 一樣相比以前排除了其它的寫與 w 併發發生,以及其它寫與r併發發生的兩種狀況 )

上面後兩條約束要強於前兩對,後者要求在 w 和 r 發生時沒有其它的 w 併發發生。在單個 goroutine 內是不可能併發的,因此單個 goroutine 的狀況下上面兩對約束是一個意思:對 v 的讀可以觀察到最近一次的 w。可是在多個 goroutine 共享 v 的狀況下, 就必須使用同步原語創建可靠的 happens-before 來保證一次讀可以讀到指定的一次寫。code

使用 v 類型的零值對v進行初始化和一次對 v 的寫操做,在內存模型中是同樣的htm

對於一個超過一個字( 即機器字 )的值來講,對它的讀寫操做至關於多個不肯定順序的單字操做

同步中的 happens before:

幾種可靠的 happens before 發生順序

1, 若是 p 導入 q 包, 那麼 q 的 init 函數是可靠發生在 p 中任何邏輯以前的
2, 而 main 包中 main 函數是可靠發生在全部 init 函數完成以後
3, goroutine 建立時的 go 聲明可靠發生在這個 goroutine 開始執行以前

var a string
func f() {
  print(a)
}
func hello() {
  a = "hello, world"   // a是被先賦值
  go f()               // go f()後執行, 因此print(a)必定會打印"hello,world"
}

4, 不使用同步機制的話, 沒法可靠保證 goroutine 的退出發生時機

var a string
func hello() {
  go func() { a = "hello" }()
  print(a)  // 這能夠打印空字符串, 也能夠打印hello, 甚至一些激進的編譯器直接刪除建立goroutine的那句
}

( 到這個位置介紹的都還比較符合直覺, 可是接下來四條結論就比較違反直覺, 至少是個人直覺, 先看結論, 後面我再結合代碼給出理解 )

5, 一次發送可靠發生在對應此次發送的接收完成以前
6, 通道關閉可靠發生在接收方收到通道類型的零值以前
7, 對於無緩衝通道的接收是可靠發生在發送完成以前
8, 第 k 次對容量爲 C 的緩衝通道的接收是可靠發生在第 k+C 次的發送完成以前

( 注: 這個位置須要對比5, 6, 7, 8的原文理解一下,
文檔原文以下:
5, A send on a channel happens before the corresponding receive from that channel completes.
6, The closing of a channel happens before a receive that returns a zero value because the channel is closed.
7, A receive from an unbuffered channel happens before the send on that channel completes.
8, The kth receive on a channel with capacity C happens before the k+Cth send from that channel completes.

理解這些要把發送分解爲兩個過程:發送,發送完成,一樣接收也是:接收,接收完成。前兩句就是一個意思, 由於第二句中通道關閉就是發送通道類型的零值, 因此前兩句就是說發送行爲是發生在接收完成以前, 即若是接收完成了,那麼發送必定發生了,但發送是否完成還不必定,強調的是接收完成的時候哪些是可靠發生了的。

var c = make(chan int, 10)
var a string
func f() {
  a = "hello"   // 寫 a 發生在發送 0 給 c 以前
  c <- 0        // 發送 0 給 c 發生在 c 接收完成以前
}
func main() {
  go f()
  <-c       // c 接收完成發生在打印 a 以前
  print(a)  // 可靠打印 hello 
}

後面7,8兩句對於非緩衝通道和緩衝通道滿了狀況的描述比較使人費解, 我在另一篇介紹通道的文檔中找到這一段

If the channel is unbuffered, the sender blocks until the receiver has received the value. If the channel has a buffer, the sender blocks only until the value has been copied to the buffer; if the buffer is full, this means waiting until some receiver has retrieved a value.

若是是無緩衝通道,發送者會一直阻塞到接收者接收完成 ( has received ) 這個值。若是是緩衝通道,發送者會一直阻塞直到值被複制到緩衝區,若是緩衝區滿了,那發送者阻塞到接收者從緩衝區中取走一個值。

這段介紹和 7,8 的結論是一致的,即對於阻塞狀態下的通道,不管是無緩衝通道仍是緩衝通道滿了,接收完成必定是先於發送完成的,這裏一直使用的是 has received 和 has retrieved, 對應 7,8 中的 completes. 因此對於無緩衝通道或者緩衝通道滿了的狀況,發送和接收最終完成,必定是接收先完成,而後發送才完成

var c = make(chan int)
var a string
func f() {
  a = "hello, world"
  <-c       // 這行先於 c<-0 完成以前完成, a 可靠賦值
}
func main() {
  go f()
  c <- 0
  // c的發送發生在print以前,
  // 而 c 的接收發生在 c 發送完成以前完成
  // 而 a 在 c 的接收以前完成賦值
  // 因此a賦值, 到c接收完成, 再到 main 中 c 發送完成, 最後可靠打印 a
  print(a)
}

另外,這段話還提供了緩衝通道的細節: 發送者等待的是把值複製到緩衝區,而不是接收者完成, 而接收者等待的是緩衝區的值, 因此對於緩衝未滿的狀況, 發送者要先完成把值複製到緩衝區,接收者才能從緩衝區讀到值,就是 5 的結論,而非緩衝通道發送者等待的是接收者完成。

這細節..., 多是爲了知道從阻塞狀態下通道解阻塞後,接收者先走一步吧

最後用圖來綜上所述吧
image

ok, 接着讀這篇內存模型的文檔 )

經過第 8 條結論, 能夠用緩衝通道來模擬計數型的同步機制:通道緩衝容量表明最大容許活躍的原語數量,達到數量以後, 若是還想使用同步原語就要等待其它活躍的同步原語被釋放,經常使用來限制併發,上代碼:

var limit = make(chan int, 3)
func main() {
  for _, w := range work {  // 雖然for爲每一個work建立了一個goroutine, 但這些goroutine並非同時活躍的   
    go func(w func()) {  
      limit <- 1            // limit滿了狀況下, goroutine就會阻塞在這裏
      w()
      <- limit              // 直到其它goroutine執行完w(), 從limit中取一個值出來, 任什麼時候候最大活躍 worker 只有3個
    }(w)
  }
}

鎖中的happens before:

9, 對於 Mutex 或者 RWMutex 類型 l, 和整型 n, m, 其中n<m, n次對 l.Unlock( ) 可靠發生在 m 次的 l.Lock( ) 以前

var l sync.Mutex
var a string
func f() {
    a = "hello"
    l.Unlock()  // n = 1
}
func main() {
    l.lock()  // m = 1 
    go f()
    l.lock()  // m = 2 上面n=1可靠發生在m=2以前, 因此可靠打印hello
    print(a)
}

10, For any call to l.RLock on a sync.RWMutex variable l, there is an n such that the l.RLock happens (returns) after call n to l.Unlock and the matching l.RUnlock happens before call n+1 to l.Lock.
這句意思是下圖
image

Once中的 happens before:

11, Once 提供了併發場景下的初始化方案, 多個 goroutine 調用 once.Do(f), 僅會有一個真正執行了 f( ), 其它的 goroutine 會阻塞等待執行的那個返回, 即其中一個真正執行的那個 goroutine 執行 f( ) 會發生在任何一 once.Do(f)返回以前

var a string
var once sync.Once
func setup() {
    a = "hello"
}
func doprint() {
    once.Do(setup)
    print(a)
}
func twoprint() {
    go doprint()    // 這兩個goroutine中僅有一個真正執行了setup(),可是兩個都會阻塞到setup()被執行完成
    go doprint()    // 因此a寫入發生在once.Do(setup)以前,print(a)會可靠打印兩遍hello
}

不正確的同步:

var a, b int
func f() {
    a = 1
    b = 2
}
func g() {
    print(b)
    print(a)
}
func main() {
    go f()
    g()    // 這個位置幾乎可print任何組合, 0-0, 0-1, 2-0, 1-2, 由於f的goroutine和主goroutine沒有任何同步,
}
var a string
var done bool

func setup() {
    a = "hello"
    done = true
}
func doprint() {
    if !done {          // 重點是, 這個邏輯是在暗示讀到了done就能讀到在done以前寫的a, 其實是,在沒有同步機制下, 讀到了done也不必定
        once.Do(setup)  // 能讀到a      
    }
    print(a)
}
func twoprint() {
    go doprint()   // 可能兩個goroutine都會阻塞在once.Do(setup)位置, 其中一個真正執行了setup, 而另外一個不會執行, 這個爲執行的goroutine
    go doprint()   // 就沒法可靠觀察到那個執行setup的goroutine對a的寫, 因此會有一個空字符串
}
var a string
var done bool

func setup() {
    a = "hello"
    done = true
}
func main() {
    go setup()
    for !done {}   // 這個也是在暗示讀到done就能讀到a,一樣這個done可能被main goroutine讀到, 但不必定表示就能讀到a, 還有就是這個done
    print(a)       // 也有可能永遠不會被main讀到,
}
type T struct {
    msg string
}
var g *T
func setup() {
    t := new(T)
    t.msg = "hello"
    g = t
}
func main() {
    go setup()
    for g == nil {}    //  main gorotine和setup gorotine共享了g, 因此main能夠觀察到g, 可是對g.msg的寫沒法可靠保證。
    print(g.msg)
}

只要顯式使用同步原語就能夠解決上面的問題

相關文章
相關標籤/搜索