日本综合一区二区|亚洲中文天堂综合|日韩欧美自拍一区|男女精品天堂一区|欧美自拍第6页亚洲成人精品一区|亚洲黄色天堂一区二区成人|超碰91偷拍第一页|日韩av夜夜嗨中文字幕|久久蜜综合视频官网|精美人妻一区二区三区

RELATEED CONSULTING
相關咨詢
選擇下列產品馬上在線溝通
服務時間:8:30-17:00
你可能遇到了下面的問題
關閉右側工具欄

新聞中心

這里有您想知道的互聯網營銷解決方案
Go內存模型并發(fā)可見性

TLTR

  • 協程之間的數據可見性滿足HappensBefore法則,并具有傳遞性
  • 如果包 p 導入包 q,則 q 的 init 函數的完成發(fā)生在任何 p 的操作開始之前
  • main.main 函數的啟動發(fā)生在所有 init 函數完成之后
  • go 語句啟動新的協程發(fā)生在新協程啟動開始之前
  • go 協程的退出并不保證發(fā)生在任何事件之前
  • channel 上的發(fā)送發(fā)生在對應 channel 接收之前
  • 無buffer channel 的接收發(fā)生在發(fā)送操作完成之前
  • 對于容量為C的buffer channel來說,第k次從channel中接收,發(fā)生在第 k + C 次發(fā)送完成之前。
  • 對于任何的 sync.Mutex 或者 sync.RWMutex 變量 ,且有 n,第 n 個調用 UnLock 一定發(fā)生在 m  Lock`之前。
  • 從 once.Do(f) 對 f() 的單個調用返回在任何一個 once.Do(f) 返回之前。
  • 如果兩個動作不滿足HappensBefore,則順序無法預測

介紹

Go內存模型指定了在何種條件下可以保證在一個 goroutine 中讀取變量時觀察到不同 goroutine 中寫入該變量的值。

建議

通過多個協程并發(fā)修改數據的程序必須將操作序列化。為了序列化訪問,通過channel操作或者其他同步原語( sync 、 sync/atomic )來保護數據。

如果你必須要閱讀本文的其他部分才能理解你程序的行為,請盡量不要這樣...

Happens Before

在單個 goroutine 中,讀取和寫入的行為必須像按照程序指定的順序執(zhí)行一樣。 也就是說,只有當重新排序不會改變語言規(guī)范定義的 goroutine 中的行為時,編譯器和處理器才可以重新排序在單個 goroutine 中執(zhí)行的讀取和寫入。 由于這種重新排序,一個 goroutine 觀察到的執(zhí)行順序可能與另一個 goroutine 感知的順序不同。 例如,如果一個 goroutine 執(zhí)行 a = 1; b = 2;,另一個可能會在 a 的更新值之前觀察到 b 的更新值。

為了滿足讀寫的需求,我們定義了 happens before ,Go程序中內存操作的局部順序。如果事件 e1 在 e2 之前發(fā)生,我們說 e2 在 e1 之后發(fā)生。還有,如果 e1 不在 e2 之前發(fā)生、 e2 也不在 e1 之前發(fā)生,那么我們說 e1 和 e2 并發(fā)happen。

在單個 goroutine 中, happens-before 順序由程序指定。

當下面兩個條件滿足時,變量 v 的閱讀操作 r 就 可能 觀察到寫入操作 w

  • r 不在 w 之前發(fā)生
  • 沒有其他的請求 w2 發(fā)生在 w 之后, r 之前

為了保證 r 一定能閱讀到 v ,保證 w 是 r 能觀測到的唯一的寫操作。當下面兩個條件滿足時, r 保證可以讀取到 w

  • w 在 r 之前發(fā)生
  • 任何其他對共享變量 v 的操作,要么在 w 之前發(fā)生,要么在 r 之后發(fā)生

這一對條件比上一對條件更強;這要求無論是 w 還是 r ,都沒有相應的并發(fā)操作。

在單個 goroutine 中,沒有并發(fā)。所以這兩個定義等價:讀操作 r 能讀到最近一次 w 寫入 v 的值。但是當多個 goroutine 訪問共享變量時,它們必須使用同步事件來建立 happens-before 關系。

使用變量 v 類型的0值初始化變量 v 的行為類似于內存模型中的寫入。

對于大于單個機器字長的值的讀取和寫入表現為未指定順序的對多個機器字長的操作。

同步

初始化

程序初始化在單個 goroutine 中運行,但該 goroutine 可能會創(chuàng)建其他并發(fā)運行的 goroutine。

如果包 p 導入包 q,則 q 的 init 函數的完成發(fā)生在任何 p 的操作開始之前。

main.main 函數的啟動發(fā)生在所有 init 函數完成之后。

Go協程的創(chuàng)建

go 語句啟動新的協程發(fā)生在新協程啟動開始之前。

舉個例子

 
 
 
  1. var a string 
  2.  
  3. func f() { 
  4.     print(a) 
  5.  
  6. func hello() { 
  7.     a = "hello, world" 
  8.     go f() 

調用 hello 將會打印 hello, world 。當然,這個時候 hello 可能已經返回了。

Go協程的銷毀

go 協程的退出并不保證發(fā)生在任何事件之前

 
 
 
  1. var a string 
  2.  
  3. func hello() { 
  4.     go func() { a = "hello" }() 
  5.     print(a) 

對 a 的賦值之后沒有任何同步事件,因此不能保證任何其他 goroutine 都會觀察到它。 事實上,激進的編譯器可能會刪除整個 go 語句。

如果一個 goroutine 的效果必須被另一個 goroutine 觀察到,請使用同步機制,例如鎖或通道通信來建立相對順序。

通道通信

通道通信是在go協程之間傳輸數據的主要手段。在特定通道上的發(fā)送總有一個對應的channel的接收,通常是在另外一個協程。

channel 上的發(fā)送發(fā)生在對應 channel 接收之前

 
 
 
  1. var c = make(chan int, 10) 
  2. var a string 
  3.  
  4. func f() { 
  5.     a = "hello, world" 
  6.     c <- 0 
  7.  
  8. func main() { 
  9.     go f() 
  10.     <-c 
  11.     print(a) 

程序能保證輸出 hello, world 。對a的寫入發(fā)生在往 c 發(fā)送數據之前,往 c 發(fā)送數據又發(fā)生在從 c 接收數據之前,它又發(fā)生在 print 之前。

channel 的關閉發(fā)生在從 channel 中獲取到0值之前

在之前的例子中,將 c<-0 替換為 close(c) ,程序還是能保證輸出 hello, world

無buffer channel 的接收發(fā)生在發(fā)送操作完成之前

這個程序,和之前一樣,但是調換發(fā)送和接收操作,并且使用無buffer的channel

 
 
 
  1. var c = make(chan int) 
  2. var a string 
  3.  
  4. func f() { 
  5.     a = "hello, world" 
  6.     <-c 
  7.  
  8. func main() { 
  9.     go f() 
  10.     c <- 0 
  11.     print(a) 

也保證能夠輸出 hello, world 。對a的寫入發(fā)生在c的接收之前,繼而發(fā)生在c的寫入操作完成之前,繼而發(fā)生在print之前。

如果該 channel 是buffer channel (例如: c=make(chan int, 1) ),那么程序就不能保證輸出 hello, world ??赡軙蛴】兆址?、崩潰等等。從而,我們得到一個相對通用的推論:

對于容量為C的buffer channel來說,第k次從channel中接收,發(fā)生在第 k + C 次發(fā)送完成之前。

此規(guī)則將先前的規(guī)則推廣到緩沖通道。 它允許通過buffer channel 來模擬信號量:通道中的條數對應活躍的數量,通道的容量對應于最大并發(fā)數。向channel發(fā)送數據相當于獲取信號量,從channel中接收數據相當于釋放信號量。 這是限制并發(fā)的常用習慣用法。

該程序為工作列表中的每個條目啟動一個 goroutine,但是 goroutine 使用 limit channel進行協調,以確保一次最多三個work函數正在運行。

 
 
 
  1. var limit = make(chan int, 3) 
  2.  
  3. func main() { 
  4.     for _, w := range work { 
  5.         go func(w func()) { 
  6.             limit <- 1 
  7.             w() 
  8.             <-limit 
  9.         }(w) 
  10.     } 
  11.     select{} 

sync 包中實現了兩種鎖類型: sync.Mutex 和 sync.RWMutex

對于任何的 sync.Mutex 或者 sync.RWMutex 變量 ,且有 n,第 n 個調用 UnLock 一定發(fā)生在 m  Lock`之前。

 
 
 
  1. var l sync.Mutex 
  2. var a string 
  3.  
  4. func f() { 
  5.     a = "hello, world" 
  6.     l.Unlock() 
  7.  
  8. func main() { 
  9.     l.Lock() 
  10.     go f() 
  11.     l.Lock() 
  12.     print(a) 

這個程序也保證輸出 hello,world 。第一次調用 unLock 一定發(fā)生在第二次 Lock 調用之前

對于任何 sync.RWMutex 的 RLock 方法調用,存在變量n,滿足 RLock 方法發(fā)生在第 n 個 UnLock 調用之后,并且對應的 RUnlock 發(fā)生在第 n+1 個 Lock 方法之前。

Once

在存在多個 goroutine 時, sync 包通過 once 提供了一種安全的初始化機制。對于特定的 f ,多個線程可以執(zhí)行 once.Do(f) ,但是只有一個會運行 f() ,另一個調用會阻塞,直到 f() 返回

從 once.Do(f) 對 f() 的單個調用返回在任何一個 once.Do(f) 返回之前。

 
 
 
  1. var a string 
  2. var once sync.Once 
  3.  
  4. func setup() { 
  5.     a = "hello, world" 
  6.  
  7. func doprint() { 
  8.     once.Do(setup) 
  9.     print(a) 
  10.  
  11. func twoprint() { 
  12.     go doprint() 
  13.     go doprint() 

調用 twoprint 將只調用一次 setup。 setup 函數將在任一打印調用之前完成。 結果將是 hello, world 打印兩次。

不正確的同步

注意,讀取 r 有可能觀察到了由寫入 w 并發(fā)寫入的值。盡管觀察到了這個值,也并不意味著 r 后續(xù)的讀取可以讀取到 w 之前的寫入。

 
 
 
  1. var a, b int 
  2.  
  3. func f() { 
  4.     a = 1 
  5.     b = 2 
  6.  
  7. func g() { 
  8.     print(b) 
  9.     print(a) 
  10.  
  11. func main() { 
  12.     go f() 
  13.     g() 

有可能 g 會接連打印2和0兩個值。

雙檢查鎖是為了降低同步造成的開銷。舉個例子, twoprint 方法可能會被誤寫成

 
 
 
  1. var a string 
  2. var done bool 
  3.  
  4. func setup() { 
  5.     a = "hello, world" 
  6.     done = true 
  7.  
  8. func doprint() { 
  9.     if !done { 
  10.         once.Do(setup) 
  11.     } 
  12.     print(a) 
  13.  
  14. func twoprint() { 
  15.     go doprint() 
  16.     go doprint() 

因為沒有任何機制保證,協程觀察到done為true的同時可以觀測到a為 hello, world ,其中有一個 doprint 可能會輸出空字符。

另外一個例子

 
 
 
  1. var a string 
  2. var done bool 
  3.  
  4. func setup() { 
  5.     a = "hello, world" 
  6.     done = true 
  7.  
  8. func main() { 
  9.     go setup() 
  10.     for !done { 
  11.     } 
  12.     print(a) 

和以前一樣,不能保證在 main 中,觀察對 done 的寫入意味著觀察對 a 的寫入,因此該程序也可以打印一個空字符串。 更糟糕的情況下,由于兩個線程之間沒有同步事件,因此無法保證 main 會觀察到對 done 的寫入。 main 中的循環(huán)會一直死循環(huán)。

下面是該例子的一個更微妙的變體

 
 
 
  1. type T struct { 
  2.     msg string 
  3.  
  4. var g *T 
  5.  
  6. func setup() { 
  7.     t := new(T) 
  8.     t.msg = "hello, world" 
  9.     g = t 
  10.  
  11. func main() { 
  12.     go setup() 
  13.     for g == nil { 
  14.     } 
  15.     print(g.msg) 

盡管 main 觀測到g不為nil,但是也沒有任何機制保證可以讀取到t.msg。

在上述例子中,解決方案都是相同的:請使用顯式的同步機制。


網站題目:Go內存模型并發(fā)可見性
本文鏈接:http://www.dlmjj.cn/article/djdjojs.html