Golang內存模型

Ref: https://golang.org/ref/memgolang

 

  1. 簡介

golang內存模型,主要說明了以下問題。在一個goroutine中讀取變量,而該變量是由其餘goroutine賦值的,這種狀況下如何可以安全正確的讀取。編程

  1. 建議

對於有多個goroutine在使用的變量,修改時須要序列化的讀取。緩存

主要方式包括,經過channel的方式、sync/atomic等原子同步操做等安全

若是你想讀完如下內容,以便理解你的程序內在運行機制,說明你很聰明。併發

可是不建議你這麼聰明~編程語言

  1. 歷史經驗

只有一個goroutine的時候,讀寫操做都會如程序定義的順序執行。這是由於,儘管編譯器和中央處理器是可能會改變執行順序,但並不會影響編程語言定義的goroutine中的行爲邏輯。但也是由於可能改變執行順序,一樣的操做在不一樣的goroutine中觀察到的執行順序並不一致。好比A goroutine執行了a=1;b=2;另外一個goroutine觀察到的結果多是b先於a被賦值了函數

爲具體說明讀寫操做的要求,咱們定義了以前某個版本中Golang程序中內存操做的一部分邏輯以下。若是事件e1發生在事件e2以前,咱們說e2發生在e1以後.若是e1並不發生在e2以前,也不發生e2在以後,咱們說e1和e2是同步發生的。atom

在一個單goroutine的程序中,事件發生的順序就是程序描述的順序。spa

知足以下兩個條件的狀況下,對變量v的讀取操做r,是可以觀察到對v的寫入操做w的:線程

  1. r並不發生在w以前;
  2. r以前,w以後,再沒有其餘對v的寫操做;

爲了保證對變量v對讀取操做r,可以觀察到特定的對v得寫操做w,須要保證w是r惟一可以觀察到寫操做。所以,要保證r可以觀察到w須要知足以下兩個條件:

  1. w發生在r以前;
  2. 其餘對v的寫操做,要麼發生在w以前,要麼發生在r以後;

這對條件是比第一個條件更加嚴格,它要求r和w的同時,沒有其它的寫操做(即和r或w同步的寫操做);

在單一goroutine裏面,沒有同步操做,因此以上兩組條件是等價的。可是對於多goroutine,須要經過同步事件來肯定順序發生,從而保證讀操做可以觀察到寫操做。

在內存模型裏面,變量v初始化爲零值,也是一種寫操做。

讀寫大於單一機器碼的變量的動做,實際操做順序不定,

 

  1. 同步
    • 初始化

程序初始化在單一goroutine裏面,可是goroutine會建立其餘goroutine,而後多個goroutine同步執行。

若是package p引用了package q,q的init函數的執行,會先於全部p的函數執行。

Main.main函數的執行,在全部init函數執行完後。

    • Goroutine的建立

go表達式,會建立一個goroutine,而後該goroutine才能開始執行。

var a string

func f() {
        print(a)
}

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

 

以上代碼示例,調用hello函數,可能在hello已經return到時候,f纔回執行print。

    • Goroutine的銷燬

goroutine的退出時機並無保證必定會在某個事件以前。

var a string

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

好比以上示例,對a的賦值,並不保證與hello自己的任何動做保持同步關係,因此也不能保證被其餘任何goroutine的讀操做觀察到。事實上,任何一個激進的編譯器都會把這裏整個go表達式直接刪掉,不作編譯。

若是一個goroutine的影響想被其餘的goroutine觀察到,必須經過同步機制(好比鎖、channel)來肯定相對順序關係。

    • Channel通訊

channel通訊是goroutines之間主要的同步方式。通常來講channel上的每一次send都會相應有另外一個goroutine今後channel受到消息。

同一個channel上,send操做老是先於相應的receive操做完成。

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

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

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

 

以上示例,可以保證print出『hello, world』。對a的寫,是先於往c中發送0,而從c中接收值,先於print。

channel的關閉,先於接收到該channel關閉而發出來的0.

在上面這個例子中用close(c)代替 c<-0,其效果是同樣的。

對於沒有緩存的channel,receive發生在send完成以前。

var c = make(chan int)
var a string

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

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

 

以上示例,依舊可以保證print出『hello, world』。對a的寫,先於從c接收;從c接收,先於 c <- 0執行完 c <- 0執行完,先於print執行。

 

但若是channel是緩存的(例如c = make(chan int, 1)),那麼以上程序不能保證print出『hello, world』,甚至有可能出現空值、crash等狀況;

對於緩存容量爲C的channel,第k次接收,先於K+C次發送完成

這條規則歸納了緩存和非緩存channel的規則。所以基於帶緩存的channel,能夠實現令牌策略:在channel中緩存的數量表明active的數量;channel的緩存容量表示最大可使用的數量;發送消息表示申請了一個令牌,接收消息表示釋放了一塊令牌。這是限制併發經常使用的一種手段。

var limit = make(chan int, 3)

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

 

以上示例程序,對於work list中的每一條,都建立了一個goroutine,可是用limit這個帶緩存的channel來限制了,最多同時只能有3個goroutines來執行work方法

 

    • 鎖機制

sync包中實現了兩個鎖的數據類型,分別是sync.Mutex和sync.RWMutex

對於任何的sync.Mutex和sync.RWMutex類型變量l,和n<m,對於l.Unlock()的調用n,老是先於對於l.Lock()的調用m

var l sync.Mutex
var a string

func f() {
        a = "hello, world"
        l.Unlock()
}

func main() {
        l.Lock()
        go f()
        l.Lock()
        print(a)
}

 

如上示例可以保證print出『hello, world』。f中第一個l.Unlock()的調用,先於main中第二個l.Lock()的調用;第二個l.Lock()的調用先於print的調用;

任何對於l.Rlock的調用(其中l爲sync.RWMutex類型變量),老是有一個n,l.Lock在調用n執行l.Unlock以後才能return;對應的,l.RUnlock的執行在調用n+1執行l.Unlock以前

 

    • 單例(Once

Sync包提供了一種安全的多goroutine種初始化機制,那就是Once類型。對於特定的方法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()
}

 

這裏print兩次『hello, world』,但只有第一次調用doprint會執行setup賦值。

 

  1. 不正確的同步

對於同步發生的讀操做r和寫操做w,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.這個事實顛覆了咱們的一些習慣認知

 

對於同步問題加鎖必定要double check

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

如這個例子,不能保證觀察到done的寫操做時候,也能觀察到對a的寫操做。其中一個goroutine可能打印出空字符串。

 

另一種錯誤的典型以下:

var a string
var done bool

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

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

 

同上一個例子同樣,這裏對done得寫觀察,不能保證對a的寫觀察,因此也可能打印出空字符串。

更甚,因爲main和setup兩個線程間沒有同步事件,並不能保證main中必定能觀察到done的寫操做,所以main中的一直循環下去沒有結束。(這裏不是很理解,只能說setup的執行時機和main中for循環沒有明確的相對前後和相對距離,因此可能致使循環好久setup還沒執行,或執行了可是沒有更新到main所讀取的done

還有以上風格的一些變體,以下:

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的賦值而退出循環,可是也不能保證觀察到g.msg的初始化值

 

對於以上全部例子,解決方案是同樣的,定義明確的同步機制。

相關文章
相關標籤/搜索