Golang內存模型(Memory Model)


1. 如何順序控制goroutine

如何保證在一個 goroutine 中看到在另一個 goroutine 修改的變量的值,如果程序中修改數據時有其他 goroutine 同時讀取,那么必須將讀取串行化。為了串行化訪問,請使用 channel 或其他同步原語,例如 sync 和 sync/atomic 來保護數據。

Happen-Before

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

[]( ̄▽ ̄)* 對於happen-before先行發生這個詞,為了更好的描述happen-before我還是決定引用權威的官方文檔,all right,let's do it!!

ψ(`∇´)ψ

2. 內存模型官方文檔

核心內容引用都來自於go的官方文檔[https://golang.org/ref/mem]

官方文檔的開篇就對go的內存模型做了一個簡單的介紹

The Go memory model specifies the conditions under which reads of a variable in one goroutine can be guaranteed to observe values produced by writes to the same variable in a different goroutine.

Programs that modify data being simultaneously accessed by multiple goroutines must serialize such access.

To serialize access, protect the data with channel operations or other synchronization primitives such as those in the sync and sync/atomic packages.

譯文:

簡單的說就是:Go內存模型限定了一些條件 滿足這些條件 才能讓變量 安全地在不同的goroutine之間讀寫。換句話說就是如何保證在一個 goroutine中看到在另一個goroutine修改的變量的值,如果程序中修改數據時有其他 goroutine 同時讀取,那么必須將讀取串行化。為了串行化訪問,請使用 channel 或其他同步原語,例如 sync 和 sync/atomic 來保護數據。

3. Happen-Before

Within a single goroutine, reads and writes must behave as if they executed in the order specified by the program. That is, compilers and processors may reorder the reads and writes executed within a single goroutine only when the reordering does not change the behavior within that goroutine as defined by the language specification. Because of this reordering, the execution order observed by one goroutine may differ from the order perceived by another. For example, if one goroutine executes a = 1; b = 2;, another might observe the updated value of b before the updated value of a.

To specify the requirements of reads and writes, we define happens before, a partial order on the execution of memory operations in a Go program. If event e1 happens before event e2, then we say that e2 happens after e1. Also, if e1 does not happen before e2 and does not happen after e2, then we say that e1 and e2 happen concurrently.

Within a single goroutine, the happens-before order is the order expressed by the program.

A read r of a variable v is allowed to observe a write w to v if both of the following hold:

r does not happen before w.
There is no other write w' to v that happens after w but before r.
To guarantee that a read r of a variable v observes a particular write w to v, ensure that w is the only write r is allowed to observe. That is, r is guaranteed to observe w if both of the following hold:

w happens before r.
Any other write to the shared variable v either happens before w or after r.
This pair of conditions is stronger than the first pair; it requires that there are no other writes happening concurrently with w or r.

Within a single goroutine, there is no concurrency, so the two definitions are equivalent: a read r observes the value written by the most recent write w to v. When multiple goroutines access a shared variable v, they must use synchronization events to establish happens-before conditions that ensure reads observe the desired writes.

The initialization of variable v with the zero value for v's type behaves as a write in the memory model.

Reads and writes of values larger than a single machine word behave as multiple machine-word-sized operations in an unspecified order.

譯文:

在一個goroutine中,讀和寫必須按照程序指定的順序執行。也就是說,只有當內存重排沒有改變既定的代碼的邏輯順序時,編譯器和處理器才可以重新排序在單個goroutine中執行的讀寫操作。由於這種重新排序,一個goroutine觀察到的執行順序可能與另一個goroutine感知到的執行順序不同。例如,如果一個goroutine執行a=1;b=2;,那么另一個goroutine可能會在a的更新值之前觀察到b的更新值。

為了指定讀寫的要求,我們定義了在Go程序中執行內存操作的偏序。如果事件e1發生在事件e2之前,那么我們說e2發生在事件e1之后。同樣,如果e1不發生在e2之前,也不發生在e2之后,那么我們說e1和e2同時發生。

在一個goroutine中,happens before順序即是程序表示的邏輯順序。

如果以下兩個條件都成立,則允許變量v的read r觀察到w到v的寫入:

r不發生在w之前。

在w之后,r之前,沒有其他寫入w'到v的操作。

要保證變量v的read r觀察到特定的對v的write操作,請確保w是唯一允許r觀察的write。也就是說,如果以下兩個條件都成立,r保證觀察到w:

w發生在r之前。

對共享變量v的任何其他寫入要么發生在w之前,要么發生在r之后。

第二個的約定明顯比第一個要強的多;它要求沒有其他寫操作與w或r同時發生。

在單個goroutine中,沒有並發性,因此這兩個定義是等價的:read r觀察最新write w寫入v的值。當多個goroutine訪問共享變量v時,它們必須使用同步事件來建立條件,以確保read觀察到所需的寫操作。

變量v的初始化(v的類型為零值)表現為在內存模型中寫入。

對大於單個機器字的值的讀取和寫入,按照未指定的順序執行多個機器字大小的操作。

PS:對於最后的machine word的解釋,比如說是64位的操作系統,64bit=8byte,意思就是在64位的操作系統,不可能出現單個線程在寫入數據時,數據小於等於8字節的就不會出現中斷,要是寫入的數據是16字節的,對於並發的寫入而言就不知道是先寫入前一半還是后一半了。

簡單的總結一下文檔的內容:

為了說明讀和寫的必要條件,我們定義了先行發生(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

4. Memory Reordering(內存重排)

Happen-Before中所表述的內容是整個go的內存模型的核心,其中就提到一個,goroutineA中有兩個賦值操作,a=1,b=2,可能在goroutineB看來可能看到的是b=2執行在先,a=1執行在后。出現這種情況的原因就是cpu的內存重排機制。

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

舉兩個例子:

	X = 0
	for i in range(100):
	X = 1
	print X
	X = 1
	for i in range(100):
	print X	

第一個代碼段想要表述的邏輯非常簡單,把X=1給賦值100次,站在實際的視角上來審視這段代碼,這樣的操作沒有任何意義,根據上面講述的重排的理論,我們的CPU會幫我們做一個處理,有可能就變成了第二段的代碼。兩段代碼從結果上看是等價的,我這里僅僅說的是不是並發的情況下。

現在有一個問題如果此時有另外一個線程干了這么一件事情: X=0,那么這兩段代碼的結果還會等價嘛。答案是大概率不等價,編譯器是無法感知到是否有一個線程在修改一個公共變量的值的。重排導致的幺蛾子 [○・`Д´・ ○]

再舉一個例子:

var A,B int

go func(){
  A = 1
  fmt.Println(B)
}()

go func(){
  B = 1
  fmt.Println(A)
}()

代碼執行結果,是1 0? 0 1? 0 0 ? 1 1 ?實際上可能會出現是0 0。這就很奇怪了,為啥是0 0。這個結果的出現和內存重排的關系很大。

現代 CPU 為了“撫平” 內核、內存、硬盤之間的速度差異,搞出了各種策略,例如三級緩存等。速度最快的當然是CPU核心,接下來是L1Cache再接下來是L2 Cache最后是L3 Cache,L3 Cache不同的線程之間是可以共享的。

內存重排內存重排內存重排

借着這兩張圖我們回頭看一看代碼,goroutine1對A賦了值,CPU核心會快速的執行該操作,不過在打印B的值的時候,goroutine從L1 Cache開始找一只找到L3 Cache,只在L3 Cache中找到了B的默認初始值0,所以goroutine打印出了0。同理goroutine2也只能打印出0。這就是出現0 0的原因。第三張圖就解釋了文字描述的過程。對於多線程的程序,所有的CPU都會提供“鎖”支持,稱之為barrier,或者fence。它要求:barrier指令要求所有對內存的操作都必須要“擴散”到memory之后才能繼續執行其他對memory的操作。因此,我們可以用高級點的atomic compare-and-swap(CAS),或者直接用更高級的鎖,通常是標准庫提供。

反過來看如果是單線程,那么store buffer(特指L1 L2 L3 cache)是完美的,如下圖所示。

內存重排

5. 小結

go的內存模型要解決的問題就是多個線程進行原子賦值的同時,期待線程(goroutine)之間可以互相看到原子賦值之后的值,也就是可見性問題。了解machine word,machine word表示寫入操作的原子性,要么成功要么失敗,這點和mysql是一樣的。不過不建議用machine word去干一些討巧的事情,比如x86 64bit的cpu處理器,machine word是8byte,在go中,比如map,指針對象等等 這些都是8byte的,你可以很放心的去讀,因為不存在讀到一半的問題,可是要是有一個對象是16byte的,你能知道是先寫前半個還是后半個嘛,這就會導致問題了。所以還是要謹慎。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM