Go開發競態檢測科普文
一、名詞解析
1、data race: Any race is a bug
定義: ①多個線程(協程)對于同一個變量、②同時地、③進行讀/寫操作、并且④至少有一個線程進行寫操作。(也就是說,如果所有線程都是只進行讀操作,那么將不構成數據爭用)
后果: 如果發生了數據爭用,讀取該變量時得到的值將變得不可知(根據內存模型),使得該多線程程序的運行結果將完全不可預測,有一定可能會導致直接崩潰。
如何防止: 對于有可能被多個線程同時訪問的變量使用排他訪問控制,具體方法包括使用mutex(互斥量)或者使用atomic變量。
---------------------------------
作者注加一條規則:凡是若干線程(協程)對一個共享變量進行同步操作,且其中有一個是寫操作的,那么讀/寫都要考慮使用原子操作。
---------------------------------
race condition(競態條件)
讀取到數據中間狀態的的情形就是 race condition。相對于數據爭用(data race),競態條件(race condition)指的是更加高層次的更加復雜的現象,一般需要在設計并行程序時進行細致入微的分析,才能確定。(也就是隱藏得更深).
定義:受各線程上代碼執行的順序和時機的影響,程序的運行結果產生(預料之外)的變化。
后果:如果存在競態條件(race condition),多次運行程序對于同一個輸入將會有不同的結果,但結果并非完全不可預測,它將由輸入數據和各線程的執行順序共同決定。
如何預防:競態條件產生的原因很多是對于同一個資源的一系列連續操作并不是原子性的,也就是說有可能在執行的中途被其他線程搶占,同時這個“其他線程”剛好也要訪問這個資源。解決方法通常是:將這一系列操作作為一個critical section(臨界區)。
2、undefined behavior(未定義行為)
未定義行為是指執行某種計算機代碼所產生的結果,這種代碼在當前程序狀態下的行為在其所使用的語言標準中沒有規定。在 Go 的內存模型中,有race的Go程序的行為是未定義行為
3、go run/build -race(開啟race檢測)
golang在1.1之后引入了競爭檢測的概念。我們可以使用go run -race或者go build -race來進行競爭檢測。-race選項打開了data race detector用來檢查這個錯誤,而且關閉了相關的編譯器優化(作者注:就是為了不讓編譯器優化代碼,能看清楚完整代碼)。(原因是)go編譯器認為race代碼是dead code,可能直接優化掉。
4、原子性
一個或者多個操作在 CPU 執行的過程中不被中斷的特性,稱為原子性(atomicity)。這些操作對外表現成一個不可分割的整體,他們要么都執行,要么都不執行,外界不會看到他們只執行到一半的狀態。
5、原子操作
原子操作(atomic operation)指的是由多步操作組成的一個操作。如果該操作不能原子地執行,則要么執行完所有步驟,要么一步也不執行,不可能只執行所有步驟的一個子集。
在單核系統里,單個的機器指令可以看成是原子操作(如果有編譯器優化、亂序執行等情況除外),在單核CPU中, 能夠在一個指令中完成的操作都可以看作為原子操作, 因為中斷只發生在指令間;
在多核系統中,單個的機器指令就不是原子操作,因為多核系統里是多指令流并行運行的,一個核在執行一個指令時,其他核同時執行的指令有可能操作同一塊內存區域,從而出現數據競爭現象。
多核系統中的原子操作通常使用內存柵障(memory barrier)來實現,即一個CPU核在執行原子操作時,其他CPU核必須停止對內存操作或者不對指定的內存進行操作,這樣才能避免數據競爭問題。
在多核CPU的時代,體系中運行著多個獨立的CPU,即使是可以在單個指令中完成的操作也可能會被干擾. 典型的例子就是decl指令(遞減指令), 它細分為三個過程: “讀->改->寫”, 涉及兩次內存操作。如果多個CPU運行的多個進程在同時對同一塊內存執行這個指令,那情況是無法預測的。
二、代碼分析
race 多協程寫分析
golang在1.1之后引入了競爭檢測的概念。我們可以使用go run -race 或者 go build -race 來進行競爭檢測。
golang語言內部大概的實現就是同時開啟多個goroutine執行同一個命令,并且紀錄每個變量的狀態。
如果使用go run -race 1.go,將出現下列提示:
這個命令輸出了WARNING:DATA RACE
總結
本篇主要介紹了一些術語,引用了一條規則:凡是多線程對共享變量涉及到寫操作,都要考慮使用原子操作。
其次,介紹了-race選項,可以對代碼涉及競態的問題做個檢查。