Go 的內(nèi)存模型

介紹

Go 的內(nèi)存模型是可以讓多個(gè) goroutine 共享數(shù)據(jù)的,但指定了條件,在這種條件下,保證一個(gè) goroutine 中可以讀取另一個(gè) goroutine 中寫入的變量值。

建議

程序中有多個(gè) goroutine 同時(shí)去更新數(shù)據(jù)時(shí),必須用序列化的方式。比如用 channel 操作或 sync、sync/atomic 的同步原語(yǔ)。

意思是:需要同步的場(chǎng)景,使用顯式的同步!顯式的同步??!顯式的?。?!

Happens Before

Happens-before 是一個(gè)順序規(guī)范,準(zhǔn)確地說(shuō)是規(guī)定了部分順序,讓某些事件發(fā)生在另一些事件之前。

單個(gè) goroutine

在單個(gè) goroutine 中,讀和寫操作必須 表現(xiàn)得像 是他們是按程序的執(zhí)行順序進(jìn)行的。

也就是說(shuō),實(shí)際上,在單個(gè) goroutine 中編譯器和執(zhí)行器會(huì)對(duì)讀操作和寫操作的順序進(jìn)行重排,但它保證了這個(gè)重排不會(huì)改變 語(yǔ)言規(guī)范 中所定義的行為。

由于重排,一個(gè) goroutine 觀察到的執(zhí)行順序可能與另一個(gè) goroutine 的不同。
比如,一個(gè) goroutine 執(zhí)行 a = 1; b = 2,那另一個(gè) goroutine 可能先觀察到 b == 2 而此時(shí) a != 1。如下測(cè)試代碼:

var count = new(uint64)

func main() {
    for i := 0; i < 1000000; i++ {
        test()
    }
    time.Sleep(time.Second)
    fmt.Println(*count) // 輸出不為 0 
}
func test() {
    var a, b int
    go func() {
        A, B := a, b
        if B == 2 && A != 1 {
            atomic.AddUint64(count, 1)
        }
    }()
    go func() {
        a = 1
        b = 2
    }()
}

為了指定讀和寫的要求,我們?cè)?Go 中定義了 Happens before 的概念——Go 執(zhí)行內(nèi)存操作的部分順序。
如果事件 e1 在 e2 之前發(fā)生,我們可以說(shuō) e2 發(fā)生在 e1 之后。如果 e1 即不在 e2 之前發(fā)生也不在 e2 之后發(fā)生,我們稱為 e1 和 e2 是同時(shí)發(fā)生。

在單 goroutine 中,happens-before 順序就是程序語(yǔ)言所表達(dá)的順序。

當(dāng)滿足以下兩個(gè)條件時(shí),一個(gè)變量(v)的讀操作(r)允許觀察它的寫操作(w):

  • 讀不發(fā)生在寫之前;
  • 在此寫操作之后或此讀之前沒(méi)有其他的寫操作(w')發(fā)生。

為了保證上述讀操作(r)能觀察到上述的寫操作(w),即需要保證以下兩個(gè)條件:

  • 寫發(fā)生在讀之前;
  • 其他任何對(duì) v 的寫操作(w', w''...),或者發(fā)生在 w 之前,或者發(fā)生在 r 之后。

這兩個(gè)條件比上面的兩個(gè)條件更健壯。它要求了這里沒(méi)有其他的寫操作與這里的 r 和 w 并發(fā)。

在單協(xié)程中沒(méi)有并發(fā),所以讀操作(r)總能觀察到最近一次寫操作(w)的值。

而在多協(xié)程中,我們就必須用同步事件來(lái)構(gòu)建 happens-before 條件來(lái)確保讀觀察到了期望的寫入。

對(duì)于一個(gè)變量初始化為它的零值時(shí),表現(xiàn)為在內(nèi)存模型中的一個(gè)寫操作。
對(duì)一個(gè)大于 單機(jī)器字 的讀和寫操作表現(xiàn)為對(duì)一個(gè)未指明順序的 多機(jī)器字大小(multiple machine-word-sized)的操作。

同步(Synchronization)

初始化(Initialization)

  • 程序初始會(huì)運(yùn)行一個(gè)單協(xié)程,但這個(gè)協(xié)程可能會(huì)創(chuàng)建并發(fā)執(zhí)行的其他協(xié)程。
  • 如果 p 包引入了 q 包,那 q 的 init() 函數(shù)發(fā)生在 p 的任何操作之前(包括 p 的 init())。
  • main.main()(main 包下的 main())發(fā)生在所有的 init() 函數(shù)之后。

協(xié)程的創(chuàng)建

  • go 命令開(kāi)啟一個(gè) go 協(xié)程發(fā)生在此 go 協(xié)程執(zhí)行之前。(即:先創(chuàng)建再執(zhí)行。這句話的意思是說(shuō) go 協(xié)程創(chuàng)建之前的語(yǔ)句和這個(gè) go 協(xié)程沒(méi)有并發(fā)問(wèn)題)

協(xié)程的銷毀

  • 協(xié)程的退出不保證發(fā)生在程序的任何事情之前。(有點(diǎn)拗口,就是指協(xié)程的執(zhí)行時(shí)間和退出正常情況下完全獨(dú)立,沒(méi)有任何時(shí)間保證)

(我們初學(xué)協(xié)程時(shí),常常會(huì)寫 main 函數(shù)的最后一句創(chuàng)建 go 協(xié)程,結(jié)果導(dǎo)致主程序結(jié)束了協(xié)程還沒(méi)執(zhí)行。就是這個(gè)意思)

確保并發(fā)下同步的三種方式

用 channel

  • 向 channel 中寫數(shù)據(jù)發(fā)生在 對(duì)應(yīng) 的讀操作之前(即讀時(shí)保證先完成寫操作);
  • channel 的關(guān)閉操作發(fā)生在讀操作之前,由于通道關(guān)閉,返回它對(duì)應(yīng)類型的零值;
  • 從無(wú)緩沖的 channel 中讀操作,發(fā)生在向此 channel 的寫完成之前;

用鎖

sync 包提供了兩種鎖 sync.Mutexsync.RWMutex

  • Lock() 之后,并發(fā)協(xié)程中的 Unlock() 一定發(fā)生在其他協(xié)程 Lock() 之前

用 Once

var once sync.Once
...
func doprint() {
    once.Do(setup)
    print(setup)
}
  • Once 保證了 once.Do() 只執(zhí)行一次

總結(jié)

這里注意的內(nèi)容主要有以下幾點(diǎn):

  • Go 的 goroutine 是共享內(nèi)存的;
  • Happends-before 原則;
  • 編譯器和執(zhí)行器會(huì)對(duì)編碼的執(zhí)行順序進(jìn)行重排,但在單協(xié)程中對(duì)外表現(xiàn)一致;
  • 學(xué)會(huì)用幾種方式保證協(xié)程中讀寫的順序(與自己期望的一致);
  • 使用顯示的同步做同步!??!

不正確的同步:

var a string
var done bool

func setup() {
    a = "hello, world" // 
    done = true        // 由于這兩個(gè)的賦值操作不一定那個(gè)先
}

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

附:

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

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

  • 下面的內(nèi)容為golang 內(nèi)存模型的翻譯,文章讀起來(lái)有點(diǎn)繞,但是會(huì)有一定的收獲原文:https://golang....
    冰瑧閱讀 277評(píng)論 0 0
  • 1 簡(jiǎn)介 Go 內(nèi)存模型指定了一個(gè)條件,在該條件下,在一個(gè) goroutine 中一個(gè)變量的讀取可保證能夠觀測(cè)到被...
    FireflyWang閱讀 537評(píng)論 0 2
  • 原文鏈接 1. Go goroutine理解 Go語(yǔ)言最大的特色就是從語(yǔ)言層面支持并發(fā)(Goroutine),G...
    將軍紅閱讀 814評(píng)論 0 0
  • 介紹 如何保證在一個(gè)goroutine中看到在另一個(gè)goroutine修改的變量的值,這篇文章進(jìn)行了詳細(xì)說(shuō)明。 建...
    51reboot閱讀 20,008評(píng)論 11 41
  • 【耶11:18】耶和華指示我,我就知道。你將他們所行的給我指明。 他們所行的:,是指亞拿突人試圖傷害耶利米的陰謀...
    主內(nèi)信息閱讀 1,159評(píng)論 0 1

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