<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
本文翻譯了原文並加入了自己的理解。
主要介紹多個 Go協程之間對同一個變數並行讀寫時需要注意的同步措施和執行順序問題。並列出幾個常見錯誤。
Go 記憶體模型涉及到多個 Go協程之間對同一個變數的讀寫。
假如有一個變數,其中一個 Go協程(a) 寫這個變數,另一個 Go協程(b) 讀這個變數;Go 記憶體模型定義了什麼情況下 Go協程(b) 能夠確保讀取到由 Go協程(a) 寫入的值。
channel
或其他同步原語( 如 sync
和 sync/atomic
兩個包裡的那些)來保護被共用的資料。除了重排序需要理解,其餘概念其實沒那麼重要,看後面的例子就懂了。
當只有一個 Go協程時,對同一個變數的讀寫必然是按照程式碼編寫的順序來執行的。對於多個變數的讀寫,如果重新排序不影響程式碼邏輯的正常執行,編譯器和處理器可能會對多個變數的讀寫過程重新排序。
比如對於 a = 1; b = 2
這兩個語句,在同一個 Go協程裡先執行 哪個其實是沒有區別的,只要最後執行結果正確就行。
a := 1//1 b := 2//2 c := a + b //3
但是,因為重新排列執行順序的情況的存在,會導致**某個 Go協程所觀察到的執行順序可能與另一個 Go協程觀察到的執行順序不一樣。**可能另一個 Go協程 觀察到的事實是 b 的值先被更新,而 a 的值被後更新。
為了表徵讀寫需求,我們可以定義 happens-before,用來表示 Go 語言中某一小段記憶體命令的執行順序。
在只有一個 Go協程的內部,happens-before的順序就是程式碼顯式定義的順序。當 Go協程 不僅僅侷限在一個的時候,存在下面兩個規則:
v
,下面的兩個條件都滿足,則讀操作 r
允許觀察到(可能觀察到,也可能觀察不到)寫操作 w
寫入的值。r
不在 w
之前發生;w’
在 w
之後發生,也不存在 w’
在 r
之前發生。r
讀取到的是寫操作 w
寫入的值,需要確保 w
是唯一允許被 r
觀察到的寫操作。如果下面的兩個條件都滿足,則 r
保證能夠觀察到 w
寫入的值:w
發生在 r
之前;v
的寫操作要麼發生在 w
之前,要麼發生在 r
之後。規則二的條件比規則一的條件更為嚴格,它要求沒有其他的寫操作和 w、r 並行地發生。
在一個 Go協程 裡是不存在並行的,因此規則一和規則二是等效的:讀操作 r 可以觀察到最近一次寫操作 w 寫入的值。
但是,當多個協程存取一個共用變數時,就必須使用同步事件來構建 happens-before 的條件,從而保證讀操作觀察到的一定是想要的寫操作。
在記憶體模型中,變數 v
的零值初始化操作等同於一個寫操作。
如果變數的值大於單機器字(CPU 從記憶體單次讀取的位元組數),那麼 CPU 在讀和寫這個變數的時候是以一種不可預知順序的多次執行單機器字的操作,這也是 sync/atomic 包存在的價值。
程式的初始化是在一個單獨的 Go協程 中進行的,但是這個協程可以建立其他的 Go協程 並且二者並行執行。
每個包都允許有一個 init
函數,當這個包被匯入時,會執行該包的這個 init
函數,做一些初始化任務。
p
匯入了包 q
, 那麼 q
的 init
函數的執行發生在 p
的所有 init
函數的執行之前。(即包的參照鏈)main.main
的執行發生在所有的 init
函數執行完成之後。通過 go
語句啟動新的 Go協程這個動作,發生在新的 Go協程的執行之前。比如下面的例子:
var a string func f() { print(a) } func hello() { a = "hello, world" go f() }
呼叫函數 hello
會在呼叫後的某個時間點列印 “hello, world” ,這個時間點可能在 hello 函數返回之前,也可能在 hello 函數返回之後。
Go協程的退出無法確保發生在程式的某個事件之前。比如下面的例子:
var a string func hello() { go func() { a = "hello" }() print(a) }
其中 a 的賦值語句沒有任何的同步措施,因此無法保證被其他任意的 Go 協程(例如 hello
函數本身)觀察到這個賦值事件的存在。
一些激進的編譯器可能會在編譯階段刪除上面程式碼中的整個 go 語句。
如果某個 Go協程 裡發生的事件必須要被另一個 Go協程 觀察到,需要使用同步機制進行保證,比如使用鎖或者通道(channel)通訊來構建一個相對的事件發生順序。
這部分介紹通過 channel 實現並行順序控制。
通道通訊是多個 Go協程 間事件同步的主要方式。在某個特定的通道上傳送一個資料,則對應地可以在這個通道上接收一個資料,一般情況下是在不同的 Go協程 間傳送與接收。
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` 發生在讀取事件 `<-c` 之前,而 `<-c` 發生在 `print(a)`之前。通道未被讀取時協程會阻塞。
close(c)
來替代 c <- 0
語句來保證同樣的效果。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` 發生在讀取事件 `<-c` 之前,而 `<-c` 發生在寫入事件 `c <- 0` 完成之前,同時寫入事件 `c <- 0` 的完成發生在 `print` 之前。
上面的程式碼,如果通道是帶快取的(比如 `c = make(chan int, 1)`),程式將不能保證會列印出 `hello, world`,它可能會列印出空字串,也可能崩潰退出,或者表現出一些其他的症狀。
var limit = make(chan int, 3) func main() { for _, w := range work { go func(w func()) { limit <- 1 // channel裡達到3個即阻塞 w() <-limit // 取出後channel裡小於3個即可繼續 }(w) } select{} }
包 sync
實現了兩類鎖資料型別,分別是 sync.Mutex
和 sync.RWMutex
,即互斥鎖和讀寫鎖。
sync.Mutex
和 sync.RWMutex
的變數 l
,如果存在 n 和 m 且滿足 n < m
,則 l.Unlock()
的第 n 次呼叫返回發生在l.Lock()
的第 m 次呼叫返回之前。即先解開上一次鎖才能上這一次鎖。
比如下面的程式碼:
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 次呼叫返回(在函數 f 內部)發生在 `l.Lock()` 的第 2 次呼叫返回之前,後者發生在 `print` 之前。
sync.RWMutex
的變數 l
,如果 l.RLock
的呼叫返回發生在 l.Unlock
的第 n 次呼叫返回之後,那麼其對應的 l.RUnlock
發生在 l.Lock
的第 n+1 次呼叫返回之前。包 sync
還提供了 Once
型別用來保證多協程的初始化的安全。
多個 Go協程 可以並行執行 once.Do(f)
來執行函數 f
, 且只會有一個 Go協程會執行 f()
,其他的 Go 協程會阻塞到 f()
執行結束(不再執行 f
,但能得到執行結果)
f()
在 once.Do(f)
的單次呼叫返回發生在其他所有的 once.Do(f)
呼叫返回之前。比如下面的程式碼:
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
方法只執行了一次setup
函數的執行返回發生在所有的 print
呼叫之前,同時會列印出兩次 hello, world
,即當一個goroutine在執行setup
方法時候,另外一個在阻塞。對某個變數的讀操作 r 一定概率可以觀察到對同一個變數的並行寫操作 w,但是即使這件事情發生了,也並不意味著發生在 r 之後的其他讀操作可以觀察到發生在 w 之前的其他寫操作。(這裡的先後指的是程式碼裡面宣告的操作的先後順序,而不是實際執行時候的)
比如下面的程式碼:
var a, b int func f() { a = 1 b = 2 } func g() { print(b) print(a) } func main() { go f() g() }
上面的程式碼裡函數 g
可能會先列印 2(b的值),然後列印 0(a的值)。可能大家會認為既然 b 的值已經被賦值為 2 了,那麼 a 的值肯定被賦值為 1 了,但事實是兩個事件的先後在這裡是沒有辦法確定的,因為編譯器會改變執行順序。
上面的事實可以證明下面的幾個常見的錯誤。
雙重檢查鎖定嘗試避免同步帶來的開銷。比如下面的例子,twoprint 函數可能會被錯誤地編寫為:
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
函數中,觀察到對 done
的寫操作並不意味著能夠觀察到對 a
的寫操作。上面的寫法依然有可能列印出空字串。
另一個常見的錯誤用法是對某個值的迴圈檢查,比如下面的程式碼:
var a string var done bool func setup() { a = "hello, world" done = true } func main() { go setup() for !done { } print(a) }
和上一個例子類似,main
函數中觀察到對 done
的寫操作並不意味著可以觀察到對 a
的寫操作,因此上面的程式碼依然可能會列印出空字串。
更糟糕的是,由於兩個 Go協程之間缺少同步事件,main
函數甚至可能永遠無法觀察到對 done
變數的寫操作,導致 main
中的 for
迴圈永遠執行下去。
上面這個錯誤有一種變體,如下面的程式碼所示:
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
函數觀察到 g != nil
並且退出了它的 for
迴圈,依然沒有辦法保證它可以觀察到被初始化的 g.msg
值。
避免上面幾個錯誤用法的方式是一樣的:顯式使用同步語句。
通過上面所有的例子,不難看出解決多goroutine下共用資料可見性問題的方法是在存取共用資料時候施加一定的同步措施。
以上就是Golang 記憶體模型The Go Memory Model的詳細內容,更多關於Go Memory Model的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45