最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Golang?內(nèi)存模型The?Go?Memory?Model

 更新時間:2022年11月11日 15:33:29   作者:吳言SDK  
這篇文章主要為大家介紹了Golang?內(nèi)存模型The?Go?Memory?Model實例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪

1. 簡介(Introduction)

官方原文

本文翻譯了原文并加入了自己的理解。

主要介紹多個 Go協(xié)程之間對同一個變量并發(fā)讀寫時需要注意的同步措施和執(zhí)行順序問題。并列出幾個常見錯誤。

Go 內(nèi)存模型涉及到多個 Go協(xié)程之間對同一個變量的讀寫。

假如有一個變量,其中一個 Go協(xié)程(a) 寫這個變量,另一個 Go協(xié)程(b) 讀這個變量;Go 內(nèi)存模型定義了什么情況下 Go協(xié)程(b) 能夠確保讀取到由 Go協(xié)程(a) 寫入的值。

2. 建議(Advice)

  • 如果多協(xié)程并發(fā)修改數(shù)據(jù),必須保證各個步驟串行執(zhí)行(序列化訪問)。
  • 為了串行執(zhí)行,可以使用 channel 或其他同步原語( 如 sync 和 sync/atomic 兩個包里的那些)來保護(hù)被共享的數(shù)據(jù)。

3. 發(fā)生在…之前(Happens Before)

除了重排序需要理解,其余概念其實沒那么重要,看后面的例子就懂了。

3.1 重排序

當(dāng)只有一個 Go協(xié)程時,對同一個變量的讀寫必然是按照代碼編寫的順序來執(zhí)行的。對于多個變量的讀寫,如果重新排序不影響代碼邏輯的正常執(zhí)行,編譯器和處理器可能會對多個變量的讀寫過程重新排序。

比如對于 a = 1; b = 2 這兩個語句,在同一個 Go協(xié)程里先執(zhí)行 哪個其實是沒有區(qū)別的,只要最后執(zhí)行結(jié)果正確就行。

a := 1//1
b := 2//2
c := a + b //3

但是,因為重新排列執(zhí)行順序的情況的存在,會導(dǎo)致**某個 Go協(xié)程所觀察到的執(zhí)行順序可能與另一個 Go協(xié)程觀察到的執(zhí)行順序不一樣。**可能另一個 Go協(xié)程 觀察到的事實是 b 的值先被更新,而 a 的值被后更新。

3.2 happens-before

為了表征讀寫需求,我們可以定義 happens-before,用來表示 Go 語言中某一小段內(nèi)存命令的執(zhí)行順序。

  • 如果事件 e1 發(fā)生在事件 e2 之前,此時我們就認(rèn)為 e2 發(fā)生在 e1 之后。
  • 如果事件 e1 既不發(fā)生在事件 e2 之前,也不發(fā)生在 e2 之后,此時我們就認(rèn)為 e1 和 e2 同時發(fā)生(并發(fā))(并發(fā) ≠ 并行)。

3.3 規(guī)則

在只有一個 Go協(xié)程的內(nèi)部,happens-before的順序就是代碼顯式定義的順序。當(dāng) Go協(xié)程 不僅僅局限在一個的時候,存在下面兩個規(guī)則:

  • 如果存在一個變量 v,下面的兩個條件都滿足,則讀操作 r 允許觀察到(可能觀察到,也可能觀察不到)寫操作 w 寫入的值。
  • r 不在 w 之前發(fā)生;
  • 不存在其他的 w’ 在 w 之后發(fā)生,也不存在 w’ 在 之前發(fā)生。
  • 為了保證讀操作 r 讀取到的是寫操作 w 寫入的值,需要確保 w 是唯一允許被 r 觀察到的寫操作。如果下面的兩個條件都滿足,則 r 保證能夠觀察到 w 寫入的值:
  • w 發(fā)生在 r 之前;
  • 其他對共享變量 v 的寫操作要么發(fā)生在 w 之前,要么發(fā)生在 r 之后。

規(guī)則二的條件比規(guī)則一的條件更為嚴(yán)格,它要求沒有其他的寫操作和 w、r 并發(fā)地發(fā)生。

在一個 Go協(xié)程 里是不存在并發(fā)的,因此規(guī)則一和規(guī)則二是等效的:讀操作 r 可以觀察到最近一次寫操作 w 寫入的值。

但是,當(dāng)多個協(xié)程訪問一個共享變量時,就必須使用同步事件來構(gòu)建 happens-before 的條件,從而保證讀操作觀察到的一定是想要的寫操作。

在內(nèi)存模型中,變量 v 的零值初始化操作等同于一個寫操作。

如果變量的值大于單機(jī)器字(CPU 從內(nèi)存單次讀取的字節(jié)數(shù)),那么 CPU 在讀和寫這個變量的時候是以一種不可預(yù)知順序的多次執(zhí)行單機(jī)器字的操作,這也是 sync/atomic 包存在的價值。

4. 同步(Synchronization)

4.1 初始化(Initialization)

程序的初始化是在一個單獨的 Go協(xié)程 中進(jìn)行的,但是這個協(xié)程可以創(chuàng)建其他的 Go協(xié)程 并且二者并發(fā)執(zhí)行。

每個包都允許有一個 init 函數(shù),當(dāng)這個包被導(dǎo)入時,會執(zhí)行該包的這個 init 函數(shù),做一些初始化任務(wù)。

  • 如果一個包 p 導(dǎo)入了包 q, 那么 q 的 init 函數(shù)的執(zhí)行發(fā)生在 p的所有 init 函數(shù)的執(zhí)行之前。(即包的引用鏈)
  • 函數(shù) main.main 的執(zhí)行發(fā)生在所有的 init 函數(shù)執(zhí)行完成之后。

4.2 Go協(xié)程的創(chuàng)建(Goroutine creation)

通過 go 語句啟動新的 Go協(xié)程這個動作,發(fā)生在新的 Go協(xié)程的執(zhí)行之前。比如下面的例子:

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

調(diào)用函數(shù) hello 會在調(diào)用后的某個時間點打印 “hello, world” ,這個時間點可能在 hello 函數(shù)返回之前,也可能在 hello 函數(shù)返回之后。

4.3 Go協(xié)程的銷毀(Goroutine destruction)

Go協(xié)程的退出無法確保發(fā)生在程序的某個事件之前。比如下面的例子:

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

其中 a 的賦值語句沒有任何的同步措施,因此無法保證被其他任意的 Go 協(xié)程(例如 hello 函數(shù)本身)觀察到這個賦值事件的存在。

一些激進(jìn)的編譯器可能會在編譯階段刪除上面代碼中的整個 go 語句。

如果某個 Go協(xié)程 里發(fā)生的事件必須要被另一個 Go協(xié)程 觀察到,需要使用同步機(jī)制進(jìn)行保證,比如使用鎖或者信道(channel)通信來構(gòu)建一個相對的事件發(fā)生順序。

4.4 信道通信(Channel communication)

這部分介紹通過 channel 實現(xiàn)并發(fā)順序控制。

有緩存channel

信道通信是多個 Go協(xié)程 間事件同步的主要方式。在某個特定的信道上發(fā)送一個數(shù)據(jù),則對應(yīng)地可以在這個信道上接收一個數(shù)據(jù),一般情況下是在不同的 Go協(xié)程 間發(fā)送與接收。

  • 規(guī)則一:在某個信道上發(fā)送數(shù)據(jù)的事件發(fā)生在相應(yīng)的接收事件之前。
    即一定是先發(fā)送數(shù)據(jù),才能接收到數(shù)據(jù)這個順序。
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` 的打印。因為信道的寫入事件 `c <- 0` 發(fā)生在讀取事件 `<-c` 之前,而 `<-c` 發(fā)生在 `print(a)`之前。信道未被讀取時協(xié)程會阻塞。

  • 規(guī)則二:信道的關(guān)閉事件發(fā)生在從信道接收到零值(由信道關(guān)閉觸發(fā))之前。
    即一定是先關(guān)閉 channel,才能接收到零值。
    在前面的例子中,可以使用 close(c) 來替代 c <- 0 語句來保證同樣的效果。

無緩存 channel

  • 規(guī)則三:對于沒有緩存的信道,數(shù)據(jù)的接收事件發(fā)生在數(shù)據(jù)發(fā)送完成之前。
    即信道容量為0時,只有發(fā)送的信息被讀取了才算發(fā)送成功,否則阻塞。
    比如下面的代碼(類似上面給出的代碼,但是使用了沒有緩存的信道,且發(fā)送和接收的語句交換了一下):
var c = make(chan int) //容量為0,無緩存
var a string
func f() {
  a = "hello, world"
  <-c
}
func main() {
  go f()
  c <- 0
  print(a)
}

上面這段代碼依然可以保證可以打印 `hello, world`。因為信道的寫入事件 `c <- 0` 發(fā)生在讀取事件 `<-c` 之前,而 `<-c` 發(fā)生在寫入事件 `c <- 0` 完成之前,同時寫入事件 `c <- 0` 的完成發(fā)生在 `print` 之前。

上面的代碼,如果信道是帶緩存的(比如 `c = make(chan int, 1)`),程序?qū)⒉荒鼙WC會打印出 `hello, world`,它可能會打印出空字符串,也可能崩潰退出,或者表現(xiàn)出一些其他的癥狀。

規(guī)則抽象

  • 規(guī)則四:對于容量為 C 的信道,接收第 k 個元素的事件發(fā)生在第 k+C 個元素的發(fā)送之前。
    規(guī)則四是規(guī)則三在帶緩存的信道上的推廣。
  • 它使得帶緩存的信道可以模擬出計數(shù)信號量:**信道中元素的個數(shù)表示活躍數(shù),信道的容量表示最大的可并發(fā)數(shù);發(fā)送一個元素意味著獲取一個信號量,接收一個元素意味著釋放這個信號量。**這是一種常見的限制并發(fā)的用法。
  • 下面的代碼給工作列表中的每個入口都開啟一個 Go協(xié)程,但是通過配合一個固定長度的信道保證了同時最多有 3 個運行的工作(最多 3 個并發(fā))。
var limit = make(chan int, 3)
func main() {
  for _, w := range work {
    go func(w func()) {
      limit <- 1  // channel里達(dá)到3個即阻塞
      w()
      <-limit  // 取出后channel里小于3個即可繼續(xù)
    }(w)
  }
  select{}
}

4. 鎖

包 sync 實現(xiàn)了兩類鎖數(shù)據(jù)類型,分別是 sync.Mutex 和 sync.RWMutex,即互斥鎖和讀寫鎖。

  • 規(guī)則一:對于類型為 sync.Mutex 和 sync.RWMutex 的變量 l,如果存在 n 和 m 且滿足 n < m,則 l.Unlock() 的第 n 次調(diào)用返回發(fā)生在l.Lock() 的第 m 次調(diào)用返回之前。

即先解開上一次鎖才能上這一次鎖。

比如下面的代碼:

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()`的第 1 次調(diào)用返回(在函數(shù) f 內(nèi)部)發(fā)生在 `l.Lock()` 的第 2 次調(diào)用返回之前,后者發(fā)生在 `print` 之前。 

  • 規(guī)則二:存在類型 sync.RWMutex 的變量 l,如果 l.RLock 的調(diào)用返回發(fā)生在 l.Unlock 的第 n 次調(diào)用返回之后,那么其對應(yīng)的 l.RUnlock 發(fā)生在 l.Lock 的第 n+1 次調(diào)用返回之前。
    即讀鎖可以上多次,但是只要沒有全解開就不能上寫鎖,寫鎖只能上一個,不解開讀寫鎖都不能上。

5. 單次運行

包 sync 還提供了 Once 類型用來保證多協(xié)程的初始化的安全。

多個 Go協(xié)程 可以并發(fā)執(zhí)行 once.Do(f) 來執(zhí)行函數(shù) f, 且只會有一個 Go協(xié)程會運行 f(),其他的 Go 協(xié)程會阻塞到 f() 運行結(jié)束(不再執(zhí)行 f,但能得到運行結(jié)果)

  • 規(guī)則一:函數(shù) f() 在 once.Do(f) 的單次調(diào)用返回發(fā)生在其他所有的 once.Do(f) 調(diào)用返回之前。

比如下面的代碼:

func setup() {
    time.Sleep(time.Second * 2) //1
    a = "hello, world"
    fmt.Println("setup over") //2
}
func doprint() {
    once.Do(setup) //3
    fmt.Println(a) //4
    wg.Done()
}
func twoprint() {
    go doprint()
    go doprint()
}
func main() {
    wg.Add(2)
    twoprint()
    wg.Wait()
}
setup over
hello, world
hello, world
  • 上面代碼使用wg sync.WaitGroup等待兩個goroutine運行完畢,由于 setup over只輸出一次,所以setup方法只運行了一次
  • 函數(shù) setup 函數(shù)的執(zhí)行返回發(fā)生在所有的 print 調(diào)用之前,同時會打印出兩次 hello, world,即當(dāng)一個goroutine在執(zhí)行setup方法時候,另外一個在阻塞。

6. 不正確的同步方式

6.1 案例一

對某個變量的讀操作 r 一定概率可以觀察到對同一個變量的并發(fā)寫操作 w,但是即使這件事情發(fā)生了,也并不意味著發(fā)生在 r 之后的其他讀操作可以觀察到發(fā)生在 w 之前的其他寫操作。(這里的先后指的是代碼里面聲明的操作的先后順序,而不是實際執(zhí)行時候的)

比如下面的代碼:

var a, b int
func f() {
  a = 1
  b = 2
}
func g() {
  print(b)
  print(a)
}
func main() {
  go f()
  g()
}

上面的代碼里函數(shù) g 可能會先打印 2(b的值),然后打印 0(a的值)??赡艽蠹視J(rèn)為既然 b 的值已經(jīng)被賦值為 2 了,那么 a 的值肯定被賦值為 1 了,但事實是兩個事件的先后在這里是沒有辦法確定的,因為編譯器會改變執(zhí)行順序。

上面的事實可以證明下面的幾個常見的錯誤。

6.2 案例二

雙重檢查鎖定嘗試避免同步帶來的開銷。比如下面的例子,twoprint 函數(shù)可能會被錯誤地編寫為:

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 的寫操作。上面的寫法依然有可能打印出空字符串。

6.3 案例三

另一個常見的錯誤用法是對某個值的循環(huán)檢查,比如下面的代碼:

var a string
var done bool
func setup() {
    a = "hello, world"
    done = true
}
func main() {
    go setup()
    for !done {
    }
    print(a)
}

和上一個例子類似,main函數(shù)中觀察到對 done 的寫操作并不意味著可以觀察到對 a 的寫操作,因此上面的代碼依然可能會打印出空字符串。

更糟糕的是,由于兩個 Go協(xié)程之間缺少同步事件,main 函數(shù)甚至可能永遠(yuǎn)無法觀察到對 done 變量的寫操作,導(dǎo)致 main 中的 for 循環(huán)永遠(yuǎn)執(zhí)行下去。

上面這個錯誤有一種變體,如下面的代碼所示:

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并且退出了它的 for 循環(huán),依然沒有辦法保證它可以觀察到被初始化的 g.msg 值。

避免上面幾個錯誤用法的方式是一樣的:顯式使用同步語句。

7. 總結(jié)

通過上面所有的例子,不難看出解決多goroutine下共享數(shù)據(jù)可見性問題的方法是在訪問共享數(shù)據(jù)時候施加一定的同步措施。

以上就是Golang 內(nèi)存模型The Go Memory Model的詳細(xì)內(nèi)容,更多關(guān)于Go Memory Model的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Go語言題解LeetCode1051高度檢查器示例詳解

    Go語言題解LeetCode1051高度檢查器示例詳解

    這篇文章主要為大家介紹了Go語言題解LeetCode1051高度檢查器示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-12-12
  • golang獲取prometheus數(shù)據(jù)(prometheus/client_golang包)

    golang獲取prometheus數(shù)據(jù)(prometheus/client_golang包)

    本文主要介紹了使用Go語言的prometheus/client_golang包來獲取Prometheus監(jiān)控數(shù)據(jù),具有一定的參考價值,感興趣的可以了解一下
    2025-03-03
  • GO中優(yōu)雅編碼與降低圈復(fù)雜度詳析

    GO中優(yōu)雅編碼與降低圈復(fù)雜度詳析

    Go語法簡單易用,有其他編程經(jīng)驗的開發(fā)者,相信學(xué)習(xí)并快速上手Go語言的開發(fā),多數(shù)覺得不困難吧,下面這篇文章主要給大家介紹了關(guān)于GO中優(yōu)雅編碼與降低圈復(fù)雜度的相關(guān)資料,需要的朋友可以參考下
    2022-12-12
  • Go目錄文件路徑操作的實現(xiàn)

    Go目錄文件路徑操作的實現(xiàn)

    在Go語言中,可以使用絕對路徑或相對路徑來表示文件路徑,本文就來介紹一下Go目錄文件路徑操作,感興趣的可以了解一下
    2023-10-10
  • Go語言模型:string的底層數(shù)據(jù)結(jié)構(gòu)與高效操作詳解

    Go語言模型:string的底層數(shù)據(jù)結(jié)構(gòu)與高效操作詳解

    這篇文章主要介紹了Go語言模型:string的底層數(shù)據(jù)結(jié)構(gòu)與高效操作詳解,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2020-12-12
  • 基于Go語言構(gòu)建RESTful API服務(wù)

    基于Go語言構(gòu)建RESTful API服務(wù)

    在實際開發(fā)項目中,你編寫的服務(wù)可以被其他服務(wù)使用,這樣就組成了微服務(wù)的架構(gòu);也可以被前端調(diào)用,這樣就可以前后端分離。那么,本文主要介紹什么是 RESTful API,以及 Go 語言是如何玩轉(zhuǎn) RESTful API 的
    2021-07-07
  • go語言中Timer和Ticker兩種計時器的使用

    go語言中Timer和Ticker兩種計時器的使用

    go語言中有Timer和Ticker這樣的兩種計時器,兩種計時器分別實現(xiàn)了不同的計時功能,本文主要介紹了go語言中Timer和Ticker兩種計時器的使用,感興趣的可以了解一下
    2024-08-08
  • Golang中Model的具體使用

    Golang中Model的具體使用

    本文主要介紹了Golang中Model的具體使用,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-04-04
  • Go有效獲取變量類型多種方法探索

    Go有效獲取變量類型多種方法探索

    這篇文章主要介紹了Go有效獲取變量類型的多種方法探索,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2024-02-02
  • Ubuntu安裝Go語言運行環(huán)境

    Ubuntu安裝Go語言運行環(huán)境

    由于最近偏愛Ubuntu,在加上作為一門開源語言,在Linux上從源代碼開始搭建環(huán)境更讓人覺得有趣味性。讓我們直接先從Go語言的環(huán)境搭建開始
    2015-04-04

最新評論

桂东县| 北碚区| 彭泽县| 达孜县| 濮阳县| 甘泉县| 迁安市| 武强县| 赫章县| 台北市| 汾阳市| 台北市| 肃宁县| 名山县| 通辽市| 江达县| 滕州市| 措勤县| 花莲县| 三明市| 岑巩县| 柘城县| 桂林市| 沧州市| 东山县| 南雄市| 理塘县| 赤城县| 方正县| 宁国市| 济阳县| 金湖县| 新蔡县| 龙州县| 澄江县| 什邡市| 喜德县| 武陟县| 肇州县| 县级市| 黄浦区|