1. 程式人生 > 其它 >go並行程式設計-2-記憶體模型

go並行程式設計-2-記憶體模型

https://golang.org/ref/mem 如何保證在一個 goroutine 中看到在另一個 goroutine 修改的變數的值,如果程式中修改資料時有其他 goroutine 同時讀取,那麼必須將讀取序列化。為了序列化訪問,請使用 channel 或其他同步原語,例如 sync 和 sync/atomic 來保護資料。

Happen-Before 在一個 goroutine 中,讀和寫一定是按照程式中的順序執行的。即編譯器和處理器只有在不會改變這個 goroutine 的行為時才可能修改讀和寫的執行順序。由於重排,不同的 goroutine 可能會看到不同的執行順序。例如,一個goroutine 執行 a = 1;b = 2;,另一個 goroutine 可能看到 b 在 a 之前更新。

 

 使用者寫下的程式碼,先要編譯成彙編程式碼,也就是各種指令,包括讀寫記憶體的指令。CPU 的設計者們,為了榨乾 CPU 的效能,無所不用其極,各種手段都用上了,你可能聽過不少,像流水線、分支預測等等。其中,為了提高讀寫記憶體的效率,會對讀寫指令進行重新排列,這就是所謂的記憶體重排,英文為 MemoryReordering。

這一部分說的是 CPU 重排,其實還有編譯器重排。比如: 

 

 

 

 

 

 

 在多核心場景下,沒有辦法輕易地判斷兩段程式是“等價”的。

現代 CPU 為了“撫平” 核心、記憶體、硬碟之間的速度差異,搞出了各種策略,例如三級快取等。為了讓 (2) 不必等待 (1) 的執行“效果”可見之後才能執行,我們可以把 (1) 的效果儲存到 store buffer:

 

先執行 (1) 和 (3),將他們直接寫入 store buffer,接著執行 (2) 和 (4)。“奇蹟”要發生了:

(2) 看了下 store buffer,

並沒有發現有 B 的值,於是從 Memory 讀出了 0,(4) 同樣從 Memory 讀出了 0。最後,

打印出了 00。

 

 

因此,對於多執行緒的程式,所有的 CPU 都會提供“鎖”支援,稱之為 barrier,或者 fence。它要求:

barrier 指令要求所有對記憶體

的操作都必須要“擴散”到 memory 之後才能繼續執行其他對 memory 的操作。因此,我們可以用高階點的

atomic compare-and-swap,

或者直接用更高階的鎖,通常是標準庫提供。

 

為了說明讀和寫的必要條件,我們定義了先行發生(Happens Before)。如果事件 e1 發生在 e2 前,我們可以說 e2 發生在 e1 後。如果 e1不發生在 e2 前也不發生在 e2 後,我們就說 e1 和 e2 是併發的。

在單一的獨立的 goroutine 中先行發生的順序即是程式中表達的順序。

當下麵條件滿足時,對變數 v 的讀操作 r 是被允許看到對 v 的寫操作 w 的:

r 不先行發生於 w

在 w 後 r 前沒有對 v 的其他寫操作

為了保證對變數 v 的讀操作 r 看到對 v 的寫操作 w,要確保 w 是 r 允許看到的唯一寫操作。即當下麵條件滿足時,r 被保證看到 w:

w 先行發生於 r

其他對共享變數 v 的寫操作要麼在 w 前,要麼在 r 後。

 

這一對條件比前面的條件更嚴格,需要沒有其他寫操作與 w 或 r 併發發生。

單個 goroutine 中沒有併發,所以上面兩個定義是相同的:

讀操作 r 看到最近一次的寫操作 w 寫入 v 的值。

當多個 goroutine 訪問共享變數 v 時,它們必須使用同步事件來建立先行發生這一條件來保證讀操作能看到需要的寫操作。 

對變數 v 的零值初始化在記憶體模型中表現的與寫操作相同。

對大於 single machine word 的變數的讀寫操作表現的像以不確定順序對多個 single machine word 的變數的操作。

 

https://www.jianshu.com/p/5e44168f47a3