The Go Memory Model(go 內存模型)


更好的閱讀體驗建議點擊下方原文鏈接。
原文鏈接:http://maoqide.live/post/golang/the-go-memory-model/

[譯]https://golang.google.cn/ref/mem

Go內存模型指定了一個條件,在該條件下,可以保證在一個 goroutine 中讀取變量,能夠獲取到另一個不同 goroutine 寫入同一變量產生的值。

Introduction

Go內存模型指定了一個條件,在該條件下,可以保證在一個 goroutine 中讀取變量,能夠獲取到另一個不同 goroutine 寫入同一變量產生的值。

Advice

如果一個程序要修改被多個 goroutine 同時訪問的數據,必須序列化此類訪問。

要序列化訪問,請使用 channel 操作或其他同步原語(例如syncsync/atomic包中的那些)來保護數據。

如果您必須閱讀本文檔的其余部分以了解程序的行為,那么您就太聰明了。

別聰明。

Happens Before

在單個 goroutine 中,讀取和寫入必須表現得好像它們按程序指定的順序執行。也就是說,只有當重新排序不改變語言規范中定義的 goroutine 的行為時,編譯器和處理器才可以對在單個 goroutine 中讀取和寫入操作的執行進行重新排序。由於這種重新排序,一個 goroutine 觀察到的執行順序可能與另一個 goroutine 感知到的順序不同。例如,如果一個 goroutine 執行a = 1; b = 2;,另一個 goroutine 可能會在 a 的值更新之前觀察到 b 的更新值。

為了指定讀取和寫入的要求,我們定義 發生之前(hanppen before),Go 程序中內存操作的局部順序。如果事件 e1 在事件 e2 發生之前(hanppen before),那么我們說事件 e2 在事件 e1 發生之后(hanppen after)。另外,如果 e1 在 e2 之前沒有發生並且在 e2 之后沒有發生,那么我們說 e1 和 e2 同時發生。

在單個goroutine中,happens-before 順序是程序表達的順序。

如果以下兩個都成立,則允許變量 v 的讀取操作 r 觀察到寫入操作 w 寫入到 v 的值:
1. r 沒有發生在 w 之前。
2. 在 w 之后但在 r 之前沒有其他寫入操作 w'。

為了保證對變量 v 的讀取操作 r 觀察到特定寫入操作 w 對 v 寫入的值,確保 w 是允許讀取操作 r 觀察到的唯一的寫入操作。也就是說,如果以下兩個條件都成立,才能保證讀取操作 r 能夠觀察到寫入操作 w:
1. w 發生在 r 之前。
2. 任何其他對共享變量 v 的寫入操作,要么發生在 w 之前,要么發生在 r 之后。

這組條件比第一組更加嚴格。它要求沒有其他寫入與 w 或 r 同時發生。

在單個goroutine中,沒有並發,因此這兩個定義是等效的:一個讀取操作 r 觀察最近的寫入操作 w 寫入 v 的值。

具有零值的 v 的類型的變量 v 的初始化表現為以上存儲模型中的寫入。

對於大於單個機器字的值的讀取和寫入操作,表現為以未指定順序進行的多個 機器字大小的操作。

Synchronization

Initialization

程序的初始化在單個 goroutine 中運行,但該 goroutine 可能會創建其他並發運行的 goroutine。

如果包 p 導入包 q,則 q 的 init 函數在包 p 的任何代碼開始之前完成。

函數 main.main 在所有的 init 函數完成后開始執行。

Goroutine creation

啟動新 goroutine 的 go 語句發生在該 goroutine 開始執行之前。

例如,在此程序中:

var a string

func f() {
	print(a)
}

func hello() {
	a = "hello, world"
	go f()
}

調用hello將在未來的某個時刻打印“hello,world”(也許在hello返回之后)。

Goroutine destruction

goroutine 的退出不保證在程序中的任何事件之前發生。例如,在此程序中:

var a string

func hello() {
	go func() { a = "hello" }()
	print(a)
}

對 a 的賦值沒有伴隨任何同步事件,因此不保證任何其他 goroutine 都能觀察到它。事實上,一個激進的編譯器可能會刪掉整條 go 語句。

如果一個 goroutine 影響必須被另一個 goroutine 觀察到,要使用鎖或 channel 通信等同步機制來建立相對順序。

Channel communication

channel 通信是 goroutine 之間同步的主要方法。特定 channel 上的每一個 send 操作都與該 channel 對應的 receive 操作相匹配,通常在不同的 goroutine 中。

channel 的 send 在該 channel 相應的 receive 操作完成之前發生

示例程序:

var c = make(chan int, 10)
var a string

func f() {
	a = "hello, world"
	c <- 0
}

func main() {
	go f()
	<-c
	print(a)
}

保證打印出 "hello, world"。對 a 的寫入發生在 c 的 send 之前,即發生在 c 的相應的 receive 完成之前,即發生在print之前。

channel 的關閉發生在因通道已關閉而接收到零值返回之前

在前面的示例中,用 close(c)替換c <- 0會產生具有保證同樣行為的程序。

無緩沖 channel 的 receive 操作在該 channel 的 send 操作完成之前發生。

示例程序(和上面一樣,但是交換了 send 和 receive 語句並且使用了無緩沖的 channel):

var c = make(chan int)
var a string

func f() {
	a = "hello, world"
	<-c
}

func main() {
	go f()
	c <- 0
	print(a)
}

同樣保證打印出 "hello, world"。對 a 的寫入發生在 c 的 receive 之前,即發生在 c 的相應的 send 完成之前,即發生在print之前。

如果 channel 是有緩沖的,(例如,c = make(chan int, 1)),那么程序將不能保證打印 "hello, world"。(可能會打印空字符串,崩潰或執行其他操作。)

具有容量C的 channel 的第 k 次 receive 操作,在第 k+C 次 send 操作完成之前。
此規則概括了先前的有緩沖的 channel 的規則。它允許用有緩沖的 channel 建立的計數信號量:channel 中的 data 數量對應於當前的使用數量,channel 的容量對應於允許最大同時使用的數量,發送一條 data 來獲取信號量,接收一條 data 來釋放信號量。這是限制並發數量的常用用法。

該程序為工作列表中的每個條目啟動一個 goroutine,但是 goroutine 利用limit這個 channel 來確保一次最多有三個正在運行的work函數。

var limit = make(chan int, 3)

func main() {
	for _, w := range work {
		go func(w func()) {
			limit <- 1
			w()
			<-limit
		}(w)
	}
	select{}
}

Locks

sync包實現了兩種鎖的類型,sync.Mutexsync.RWMutex

對於任何sync.Mutexsync.RWMutex類型的變量l,並且n < m,第n次調用l.Unock()在第m次調用l.Lock()返回之前發生。
示例程序:

var l sync.Mutex
var a string

func f() {
	a = "hello, world"
	l.Unlock()
}

func main() {
	l.Lock()
	go f()
	l.Lock()
	print(a)
}

保證打印出"hello, world"。第一次調用l.Unlock()(在f()中),在第二次調用l.Lock()(在main()中)返回之前發生,即在print之前發生。

For any call to l.RLock on a sync.RWMutex variable l, there is an n such that the l.RLock happens (returns) after call n to l.Unlock and the matching l.RUnlock happens before call n+1 to l.Lock.

對於sync.RWMutex類型的變量ll.RLock()的任意調用,有一個 n 使得本次l.RLock()在第 n 次調用l.Unlock()之后發生(返回)並且對應的l.RUnlock()在第 n+1 調用l.Lock()之前發生。

Once

sync包通過使用Once類型,在存在多個 goroutine 的情況下提供了一種安全的初始化機制。多個線程可以對特定的 f 執行nce.Do(f),但是只有一個線程會真正運行f(),並且其他調用會阻塞直到f()返回。

once.Do(f) 中對f()的單次調用在任意once.Do(f)的調用之前發生(返回)。
在如下程序中:

var a string
var once sync.Once

func setup() {
	a = "hello, world"
}

func doprint() {
	once.Do(setup)
	print(a)
}

func twoprint() {
	go doprint()
	go doprint()
}

調用twoprint()將只會調用setup()一次。setup 方法將在print之前完成。結果是"hello, world"將被打印兩次。

Incorrect synchronization

注意,讀取操作 r 可以觀察到與r同時發生的寫入操作 w 所寫的值。即使發生這種情況,也不意味着在 r 之后發生的讀取操作將觀察到在 w 之前發生的寫入操作。
如下程序中:

var a, b int

func f() {
	a = 1
	b = 2
}

func g() {
	print(b)
	print(a)
}

func main() {
	go f()
	g()
}

可能會發生 g 先打印 2 然后打印 0。

這使一些常見的管用語法無效。

雙重檢查鎖 是為了避免同步的開銷。
例如,twoprint程序可能被錯誤的寫為:

var a string
var done bool

func setup() {
	a = "hello, world"
	done = true
}

func doprint() {
	if !done {
		once.Do(setup)
	}
	print(a)
}

func twoprint() {
	go doprint()
	go doprint()
}

但是這不能保證,在doprint中,觀察到done的寫入操作意味着同樣能觀察到對a的寫入操作。這個版本可能(錯誤地)打印空字符串而不是"hello,world"。

另一個不正確的慣用語法是忙着等待一個值,如:

var a string
var done bool

func setup() {
	a = "hello, world"
	done = true
}

func main() {
	go setup()
	for !done {
	}
	print(a)
}

像之前一樣,不能保證在main中,觀察到done的寫入操作意味着同樣能觀察到對a的寫入操作,因此這個程序也可能打印出空的字符串。更糟的是,還無法保證main能觀察到對done的寫入操作,因為兩個線程之間沒有同步事件。main中的循環無法保證能完成。

這個主題有更微妙的變體,例如這個程序:

type T struct {
	msg string
}

var g *T

func setup() {
	t := new(T)
	t.msg = "hello, world"
	g = t
}

func main() {
	go setup()
	for g == nil {
	}
	print(g.msg)
}

即使main觀察到g != nil並且退出循環,無法保證它會觀察到g.msg的初始化值。

在所有這些示例中,解決方案是相同的:使用顯式的同步。


免責聲明!

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



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