Go:從一個(gè)data race問(wèn)題學(xué)到的
前幾天我在學(xué)習(xí)內(nèi)存屏障[1]的時(shí)候搜到一篇文章「Golang Memory Model[2]」,其中在介紹 CPU 緩存一致性的時(shí)候提到一個(gè)例子,帶給我一些困惑,本文記錄下解惑過(guò)程。
既然是在介紹 CPU 緩存一致性的時(shí)候舉的例子,那么理所應(yīng)當(dāng)與此有關(guān),看代碼:
package?main
import?"time"
func?main()?{
?running?:=?true
?go?func()?{
??println("start?thread1")
??count?:=?1
??for?running?{
???count++
??}
??println("end?thread1:?count?=",?count)
?}()
?go?func()?{
??println("start?thread2")
??for?{
???running?=?false
??}
?}()
?time.Sleep(time.Hour)
}
當(dāng)我們通過(guò)「go run main.go」運(yùn)行代碼的時(shí)候,會(huì)發(fā)現(xiàn)第一個(gè) goroutine 永遠(yuǎn)不會(huì)結(jié)束,就好像 running = false 沒(méi)有生效一樣。對(duì)此,文章把原因歸結(jié)為 CPU 緩存一致性中的線(xiàn)程可見(jiàn)性問(wèn)題,可是我前后看了幾遍也沒(méi)有看出個(gè)所以然來(lái)。細(xì)心的小伙伴不難發(fā)現(xiàn)代碼存在 data race 問(wèn)題:多個(gè) goroutine 并發(fā)讀寫(xiě) running 變量,不過(guò)當(dāng)我們通過(guò)「go run -race main.go」再次運(yùn)行代碼的時(shí)候,有趣的事情發(fā)生了,第一個(gè) goroutine 正常結(jié)束了!
理論上,既然存在 data race 問(wèn)題,那么出現(xiàn)什么結(jié)果都可能,但是好奇心驅(qū)使我繼續(xù)研究了一下,這次使用的工具是 SSA[3],它可以展現(xiàn)出從源代碼到匯編的過(guò)程中,編譯器都做了哪些工作,并且可以把結(jié)果生成 html 文件:
shell>?GOSSAFUNC=main?go?build?-gcflags="-N?-l"?./main.go
SSA 工具最方便的地方是它可以把源代碼和匯編通過(guò)顏色對(duì)應(yīng)起來(lái):

說(shuō)明:Golang 中的匯編一般指 Plan9 匯編,推薦閱讀「plan9 assembly 完全解析[4]」。
不過(guò)為什么「running = false」這行源代碼沒(méi)有對(duì)應(yīng)的匯編呢?這是因?yàn)?SSA 的工作單位是函數(shù),上面的結(jié)果是 main 函數(shù)的結(jié)果,而「running = false」實(shí)際上屬于 main 函數(shù)里第 2 個(gè) goroutine,實(shí)際上就相當(dāng)于 main.func2,重新運(yùn)行 SSA:
shell>?GOSSAFUNC=main.func2?go?build?-gcflags="-l?-N"?./main.go
如此一來(lái)就能看到「running = false」這行源代碼對(duì)應(yīng)的匯編了:

其中,PCDATA 是編譯器插入的和 GC 相關(guān)的信息,在本例中可以忽略,剩下的幾個(gè) JMP 跳來(lái)跳去,好像是個(gè)圈哦,就是一個(gè)空 for,和「running = false」完全沒(méi)有關(guān)系。
不過(guò)既然帶有 race 檢測(cè)的代碼工作正常,那么不妨一并生成 SSA 看看結(jié)果如何:
shell>?GOSSAFUNC=main.func2?go?build?-race?-gcflags="-l?-N"?./main.go
結(jié)果如下圖所示,出了 JMP,還有 MOV 操作,正好對(duì)應(yīng)「running = false」:

如此一來(lái),我們的困惑終于解開(kāi)了。問(wèn)題代碼中的循環(huán)之所以不會(huì)結(jié)束,和所謂的「CPU 緩存一致性中的線(xiàn)程可見(jiàn)性問(wèn)題」并沒(méi)有任何關(guān)系,只是因?yàn)榫幾g器把部分代碼看成死代碼,直接優(yōu)化掉了,這個(gè)過(guò)程稱(chēng)之為「Dead code elimination[5]」,不過(guò)當(dāng)激活 race 檢測(cè)的時(shí)候,編譯器并沒(méi)有執(zhí)行死代碼的優(yōu)化,所以程序看上去又正常了。
最后,推薦一篇文章,和本文的例子相似:談?wù)?Golang 中的 Data Race[6](及續(xù)[7])。
參考資料
內(nèi)存屏障: https://zh.wikipedia.org/wiki/%E5%86%85%E5%AD%98%E5%B1%8F%E9%9A%9C
[2]Golang Memory Model: https://fanlv.wiki/2020/06/09/golang-memory-model/
[3]SSA: https://github.com/golang/go/blob/master/src/cmd/compile/internal/ssa/README.md
[4]plan9 assembly 完全解析: https://github.com/cch123/golang-notes/blob/master/assembly.md
[5]Dead code elimination: https://en.wikipedia.org/wiki/Dead_code_elimination
[6]談?wù)?Golang 中的 Data Race: https://ms2008.github.io/2019/05/12/golang-data-race/
[7]續(xù): https://ms2008.github.io/2019/05/22/golang-data-race-cont/
推薦閱讀
