Go內存更新問題

前言

在開始之前, 先來引出問題. 有這樣一段go代碼:

func main() {
	var wg sync.WaitGroup
	wg.Add(2)
	var x, y int
	go func() {
		defer wg.Done()
		x = 1
		fmt.Println(fmt.Sprintf("y=%d", y))
	}()
	go func() {
		defer wg.Done()
		y = 1
		fmt.Println(fmt.Sprintf("x=%d", x))
	}()
	wg.Wait()
}

這段代碼可能會有哪些結果呢? 無非看語句的執行順序嘛, 排列組合一下可能的情況:

  • x=1, y=1
  • x=0, y=1
  • x=1, y=1

但是, 如果你多跑幾次, 就會發現, x=0, y=0這種我們以爲不會出現的情況, 是真的會出現的.

不過這種情況爲什麼發生想必也都心知肚明, 就簡單聊一聊吧.

why

爲什麼會出現這種情況呢? 其實, 如果瞭解多線程及 CPU 的實現, 倒也不難理解.

首先, CPU 爲了加速, 存在多級緩存. 同樣的內存修改會先修改 CPU 內部緩存, 不會立即刷新到內存上.

此時, 若是多核 CPU, 多個線程跑在不同核上, CPU1對內存進行了修改, 但此時修改還在CPU緩存上, 沒有刷新到內存, CPU2到內存讀到的就是舊值.

同樣的, 在Go中, 多個協程也是跑在不同的 CPU 核上, 所以, 內存的更新對其他 CPU 核來說也不是立即可見的. 出現這樣的問題也就不奇怪了.

不光是寫, 讀操作也是有緩存的呦

探究

Java中, 存在volatile關鍵字, 來保證字段的更新立即刷新到內存. 那麼在Go中, 如何來解決這個問題呢?

一說到多線程同步, 第一個想到的必定就是鎖了, 沒錯lock可以保證更新立即可見, 同樣的channel atomic 都可以. 其中atomic包做的事情 和volatile是一樣的. (也就是說, 前面的例子只要在將讀寫的操作改爲atomic, 就不會出現 x=0,y=0 的情況啦).

Go官方文檔中對內存模型進行了簡單的介紹, 也說明了這種錯誤. 甚至於, 在文檔中給出了這樣的例子(感興趣的可以去看一下文檔, 還挺有趣的):

var a string 
var done bool 
func setup() { 
	a = "hello, world" 
	done = true 
} 
func main() { 
	go setup() 
	for !done { 
	} 
	print(a) 
}
  1. 結果可能打印空字符串. 也就是說, done的更新同步到內存了, 但是a沒有
  2. 甚至, 極端情況main的循環可能不會結束. 即CPU 緩存刷新時間很長.

同時, 在這篇官方文檔中還有一些很有意思的內容, 推薦讀一讀. 比如:

  1. 說明了編譯期對指令執行順序的保證
  2. 多協程通信的方式(就是我們已知的幾個lock/channel/atomic等)
  3. runtime.SetFinalizer變量的析構函數(但是如果在回收前進程就結束了, 可能不會調用)
  4. build/run命令後跟上-race參數, 可以檢測是否存在多協程變量競爭的問題. 若存在, 會在運行時報錯.
  5. 等等

over, 對此問題一個簡簡單單的回顧


原文鏈接: https://hujingnb.com/archives/879

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章