自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Go 內(nèi)存模型 并發(fā)可見性

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

[[409588]]

TLTR

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

介紹

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

建議

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

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

Happens Before

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

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

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

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

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

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

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

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

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

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

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

同步

初始化

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

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

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

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

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

舉個例子

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

調(diào)用 hello 將會打印 hello, world 。當(dāng)然,這個時候 hello 可能已經(jīng)返回了。

Go協(xié)程的銷毀

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

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

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

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

通道通信

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

channel 上的發(fā)送發(fā)生在對應(yīng) 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的寫入發(fā)生在往 c 發(fā)送數(shù)據(jù)之前,往 c 發(fā)送數(shù)據(jù)又發(fā)生在從 c 接收數(shù)據(jù)之前,它又發(fā)生在 print 之前。

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

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

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

這個程序,和之前一樣,但是調(diào)換發(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 來模擬信號量:通道中的條數(shù)對應(yīng)活躍的數(shù)量,通道的容量對應(yīng)于最大并發(fā)數(shù)。向channel發(fā)送數(shù)據(jù)相當(dāng)于獲取信號量,從channel中接收數(shù)據(jù)相當(dāng)于釋放信號量。 這是限制并發(fā)的常用習(xí)慣用法。

該程序?yàn)楣ぷ髁斜碇械拿總€條目啟動一個 goroutine,但是 goroutine 使用 limit channel進(jìn)行協(xié)調(diào),以確保一次最多三個work函數(shù)正在運(yùn)行。

  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 包中實(shí)現(xiàn)了兩種鎖類型: sync.Mutex 和 sync.RWMutex

對于任何的 sync.Mutex 或者 sync.RWMutex 變量 ,且有 n<m ,第 n 個調(diào)用 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 。第一次調(diào)用 unLock 一定發(fā)生在第二次 Lock 調(diào)用之前

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

Once

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

從 once.Do(f) 對 f() 的單個調(diào)用返回在任何一個 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() 

調(diào)用 twoprint 將只調(diào)用一次 setup。 setup 函數(shù)將在任一打印調(diào)用之前完成。 結(jié)果將是 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() 

因?yàn)闆]有任何機(jī)制保證,協(xié)程觀察到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,但是也沒有任何機(jī)制保證可以讀取到t.msg。

 

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

責(zé)任編輯:張燕妮 來源: Go語言中文網(wǎng)
相關(guān)推薦

2020-02-28 14:48:51

結(jié)構(gòu)系統(tǒng)程序

2021-05-06 19:20:05

Java內(nèi)存模型

2024-11-18 16:37:35

JMMJava內(nèi)存模型

2022-07-10 20:49:57

javaVolatile線程

2021-09-01 10:50:25

云計(jì)算云計(jì)算環(huán)境云應(yīng)用

2016-11-11 00:39:59

Java可見性機(jī)制

2018-07-19 14:34:48

數(shù)據(jù)中心監(jiān)控網(wǎng)絡(luò)

2024-02-27 17:46:25

并發(fā)程序CPU

2011-11-29 13:09:02

2022-03-24 08:02:39

網(wǎng)絡(luò)安全端點(diǎn)

2024-02-18 13:34:42

云計(jì)算

2023-06-13 08:29:18

網(wǎng)絡(luò)可見性Cato

2013-08-27 09:17:15

軟件定義網(wǎng)絡(luò)SDN網(wǎng)絡(luò)可見性

2021-12-22 11:15:04

云計(jì)算混合云公有云

2011-07-29 11:04:52

2020-08-25 09:51:40

Android 11開發(fā)者軟件

2018-12-18 14:08:01

Java內(nèi)存volatile

2018-05-26 16:01:37

2020-07-20 10:40:31

云計(jì)算云平臺IT

2016-07-04 08:19:13

混合IT網(wǎng)絡(luò)問題SaaS
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號