淺析golang如何在多線程中避免CPU指令重排
起因
golang 的發(fā)明初衷便是多線程,是一門專門用于多線程高并發(fā)的編程語言。其獨創(chuàng)的 GMP 模型在多線程的開發(fā)上提供了很大的便利。
現(xiàn)代計算機基本上都是多核 CPU 的結(jié)構(gòu)。CPU 在進行指令運行的時候,為了提高效率,會在一些情況下對指令進行重排序,其目的是在保持運行結(jié)果和不重拍序的指令一致的前提下,提高程序的運行效率。但是對于多線程并行執(zhí)行來說,我們可能需要對此額外關(guān)注,以避免重排對多線程的影響。
英特爾在其 x86/64 體系結(jié)構(gòu)規(guī)范第 3 卷 §8.2.3 中列出了幾個這樣的問題。這里有一個最簡單的例子。假設(shè)內(nèi)存中有兩個整數(shù) X 和 Y,最初的值都是 0。兩個并行運行的處理器執(zhí)行以下的機器代碼:
雖然在這個例子中使用匯編語言,但這確實是說明 CPU 排序的比較好的方式。每個處理器將 1 存儲到其中一個整數(shù)變量中,然后將另一個整數(shù)加載到寄存器中。(r1 和 r2 只是實際 x86 寄存器(如 eax)的占位符名稱。)
現(xiàn)在,無論哪個處理器先將 1 寫入內(nèi)存,都很自然地希望另一個處理器讀取回該值,這意味著我們最終應(yīng)該得到 r1=1、r2=1,或者兩者都有。但根據(jù)英特爾的規(guī)范,情況不一定如此。在規(guī)范中,在這個例子的結(jié)尾,r1 和 r2 都等于 0 是合法的!這可能是一個違反直覺的結(jié)果!
理解這一點的一種方法是,與大多數(shù)處理器系列一樣,英特爾x86/64處理器可以根據(jù)某些規(guī)則重新排序機器指令的內(nèi)存交互,只要它永遠不會改變單線程程序的執(zhí)行。特別地,允許每個處理器將存儲的效果延遲超過來自不同位置的任何加載。因此,最終可能會出現(xiàn)指令按以下順序執(zhí)行的情況:
程序測試
CPU 指令重排導致的問題
在下面的程序中,來實現(xiàn)上述 CPU 指令重排在多線程中造成的數(shù)據(jù)不一致現(xiàn)象。下面代碼中,聲明了 a,b,x,y 四個變量并將其默認值設(shè)置為 0。聲明兩個 go routine 分別執(zhí)行目標操作(見代碼)。正常情況,不管下面 a = 1,x = b,b = 1, y = a 這四條質(zhì)量如何執(zhí)行,如果沒有重排產(chǎn)生,那么永遠不可能出現(xiàn) x == 0 和 y == 0 同時發(fā)生的情況。
但是由于 CPU 指令重排的原因,在實際執(zhí)行的情況下,在第 1738, 110002, 12987 次測試到了 CPU 指令重排的發(fā)生。
func withCpuReordering() { index := 0 for { index += 1 var a, b int32 = 0, 0 var x, y int32 = 0, 0 var wg sync.WaitGroup wg.Add(2) go func() { defer wg.Done() a = 1 x = b }() go func() { defer wg.Done() b = 1 y = a }() wg.Wait() if x == 0 && y == 0 { panic("CPU Reordering occurs!") } else { fmt.Println("Now processing in loop", index) } } }
綁定 CPU 消除指令重排
上述例子的現(xiàn)象只在多核 CPU 執(zhí)行的之后才會出現(xiàn),也就是線程并行執(zhí)行的時候才會出現(xiàn)。如果我們將上述程序的執(zhí)行都鎖定在一個 CPU 上,也就能避免這種情況的發(fā)生。
在下面代碼中,我們制定 go routine 最多只能使用一個 CPU。在整個測試過程中,沒有出現(xiàn) x == 0 和 y == 0 同時發(fā)生的情況。
func main() { runtime.GOMAXPROCS(1) withCpuReordering() }
原因在于指令重排的目的在于提高執(zhí)行效率,而不是改變執(zhí)行結(jié)果。
通過內(nèi)存屏障消除指令重排
在 Go 語言的 sync/atomic 包中,原子操作函數(shù)的實現(xiàn)會使用 CPU 提供的原子操作指令,以實現(xiàn)對共享變量的原子讀寫操作。這些原子操作指令通常會在硬件層面實現(xiàn)內(nèi)存屏障(Memory Barrier),以確保對共享變量的讀寫操作在不同的 CPU 核心之間具有一定的有序性。
在下面的代碼中,通過 atomic 包中的原子操作函數(shù)代替了上述代碼中的賦值操作,從而解決了執(zhí)行結(jié)果不一致的情況。
func withoutCpuReordering() { index := 0 for { index += 1 var a, b int32 = 0, 0 var x, y int32 = 0, 0 var wg sync.WaitGroup wg.Add(2) go func() { defer wg.Done() atomic.StoreInt32(&a, 1) atomic.StoreInt32(&x, atomic.LoadInt32(&b)) }() go func() { defer wg.Done() atomic.StoreInt32(&b, 1) atomic.StoreInt32(&y, atomic.LoadInt32(&a)) }() wg.Wait() if x == 0 && y == 0 { panic("CPU Reordering occurs!") } else { fmt.Println("Now processing in loop", index) } } }
類似的指令和不同的平臺
所有這些不同的 CPU 系列,每個都有獨特的指令來強制執(zhí)行內(nèi)存排序,編譯器根據(jù)不同的 CPU 系列將代碼編譯成不同的指令,并且每個跨平臺項目都實現(xiàn)了自己的可移植層。這些都無助于簡化無鎖編程!這也是最近引入 C++11 原子庫標準的部分原因。這是一種標準化的嘗試,使編寫可移植的無鎖代碼變得更容易。
比如 mfence 指令特定于 x86/64 的 CPU 架構(gòu)。如果想使代碼更具可移植性,可以將此內(nèi)在特性封裝在預(yù)處理器宏中。Linux 內(nèi)核將其封裝在一個名為 smp_mb 的宏,以及相關(guān)的宏中,如 smp_rmb 和 smp_wmb,并在不同的體系結(jié)構(gòu)上提供了替代實現(xiàn)。例如,在 PowerPC 上,smp_mb 被實現(xiàn)為 sync。
到此這篇關(guān)于淺析golang如何在多線程中避免CPU指令重排的文章就介紹到這了,更多相關(guān)go多線程避免CPU指令重排內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Golang實現(xiàn)Mongo數(shù)據(jù)庫增刪改查操作
本文主要介紹了Golang實現(xiàn)Mongo數(shù)據(jù)庫增刪改查操作,我們使用了 MongoDB的官方Go驅(qū)動程序,實現(xiàn)了插入、查詢、更新和刪除操作,感興趣的可以了解一下2024-01-01Golang實現(xiàn)異步上傳文件支持進度條查詢的方法
這篇文章主要介紹了Golang實現(xiàn)異步上傳文件支持進度條查詢的方法,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-10-10go 代碼的調(diào)試---打印調(diào)用堆棧的實例
下面小編就為大家?guī)硪黄猤o 代碼的調(diào)試---打印調(diào)用堆棧的實例。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-10-10Go?Gin框架優(yōu)雅重啟和停止實現(xiàn)方法示例
Web應(yīng)用程序中,有時需要重啟或停止服務(wù)器,無論是因為更新代碼還是進行例行維護,這時需要保證應(yīng)用程序的可用性和數(shù)據(jù)的一致性,就需要優(yōu)雅地關(guān)閉和重啟應(yīng)用程序,即不丟失正在處理的請求和不拒絕新的請求,本文將詳解如何在Go語言中使用Gin這個框架實現(xiàn)優(yōu)雅的重啟停止2024-01-01