成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

Go 內存模型 并發可見性

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

[[409588]]

TLTR

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

介紹

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

建議

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

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

Happens Before

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

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

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

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

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

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

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

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

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

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

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

同步

初始化

程序初始化在單個 goroutine 中運行,但該 goroutine 可能會創建其他并發運行的 goroutine。

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

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

Go協程的創建

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

舉個例子

  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 協程的退出并不保證發生在任何事件之前

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

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

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

通道通信

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

channel 上的發送發生在對應 channel 接收之前

  1. var c = make(chan int10
  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的寫入發生在往 c 發送數據之前,往 c 發送數據又發生在從 c 接收數據之前,它又發生在 print 之前。

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

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

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

這個程序,和之前一樣,但是調換發送和接收操作,并且使用無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的寫入發生在c的接收之前,繼而發生在c的寫入操作完成之前,繼而發生在print之前。

如果該 channel 是buffer channel (例如: c=make(chan int, 1) ),那么程序就不能保證輸出 hello, world 。可能會打印空字符串、崩潰等等。從而,我們得到一個相對通用的推論:

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

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

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

  1. var limit = make(chan int3
  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<m ,第 n 個調用 UnLock 一定發生在 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 一定發生在第二次 Lock 調用之前

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

Once

在存在多個 goroutine 時, sync 包通過 once 提供了一種安全的初始化機制。對于特定的 f ,多個線程可以執行 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 并發寫入的值。盡管觀察到了這個值,也并不意味著 r 后續的讀取可以讀取到 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 中的循環會一直死循環。

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

  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語言中文網
相關推薦

2020-02-28 14:48:51

結構系統程序

2021-05-06 19:20:05

Java內存模型

2024-11-18 16:37:35

JMMJava內存模型

2022-07-10 20:49:57

javaVolatile線程

2021-09-01 10:50:25

云計算云計算環境云應用

2016-11-11 00:39:59

Java可見性機制

2018-07-19 14:34:48

數據中心監控網絡

2024-02-27 17:46:25

并發程序CPU

2011-11-29 13:09:02

2022-03-24 08:02:39

網絡安全端點

2024-02-18 13:34:42

云計算

2023-06-13 08:29:18

網絡可見性Cato

2025-06-04 04:10:00

HappensGo內存

2013-08-27 09:17:15

軟件定義網絡SDN網絡可見性

2020-08-25 09:51:40

Android 11開發者軟件

2021-12-22 11:15:04

云計算混合云公有云

2011-07-29 11:04:52

2018-12-18 14:08:01

Java內存volatile

2023-04-06 15:47:23

2020-07-20 10:40:31

云計算云平臺IT
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: www.日韩av.com | 日韩中文字幕一区二区 | heyzo在线 | 在线视频91| 国产在线中文字幕 | 男人天堂社区 | 91在线免费视频 | 91极品尤物在线播放国产 | 欧美日韩1区2区 | 亚洲国产成人精品女人 | 国内精品久久久久久久 | a免费视频| 91麻豆精品国产91久久久更新资源速度超快 | 羞羞视频在线观免费观看 | 福利视频一区二区 | 一区二区免费在线观看 | gogo肉体亚洲高清在线视 | www.色综合 | 蜜桃视频在线观看免费视频网站www | 欧美日一区| 亚洲精品日韩在线 | 久久高清 | 欧美亚洲第一区 | 国产婷婷色综合av蜜臀av | 久久国产福利 | 四虎影院欧美 | 亚洲国产精品自拍 | 中文字幕91av | 久久一区二区视频 | 欧美一级片a | 久久久久久国产 | 99日韩| 亚洲精品久久久一区二区三区 | 成人影院在线观看 | 久久久噜噜噜久久中文字幕色伊伊 | 精精国产xxxx视频在线野外 | h片免费看 | 欧美在线一区二区三区 | 久久久久久亚洲精品 | 精品欧美一区二区三区久久久 | 免费久久久 |