下面的內(nèi)容為golang 內(nèi)存模型的翻譯,文章讀起來有點繞,但是會有一定的收獲
介紹
Go語言的內(nèi)存模型規(guī)定了一種規(guī)則。這種規(guī)則可以保證,在一個goroutine讀取某個變量的值,是其他goroutine對同一個變量寫入的值。
建議
程序中多個goroutine同時修改相同數(shù)據(jù),必須使之能夠按序訪問。為了按序訪問以及保護數(shù)據(jù),可以使用channel操作或者其他同步原語例如sync及sync/atomic包。
If you must read the rest of this document to understand the behavior of your program, you are being too clever.
Don't be clever.
Happens-Before原則
在單個goroutine中,讀取和寫入的操作一定與程序的執(zhí)行順序一致。 也就是說,僅當重新排序語句不會改變語言規(guī)范所定義的該goroutine中的行為時,編譯器和處理器才可以對單個goroutine中執(zhí)行的讀取和寫入進行重新排序。 由于此重新排序,一個goroutine觀察到的執(zhí)行順序可能不同于另一個goroutine所察覺的執(zhí)行順序。 例如,如果一個goroutine執(zhí)行a = 1; b = 2;,另一個goroutine可能會a的值更新前,就已經(jīng)觀察到b的更新后的值。
在Go程序中,為了限定讀取和寫入的要求,我們定義happens-before原則,此原則限定了程序在讀寫內(nèi)存時的一種偏序關(guān)系。 如果事件e1發(fā)生在事件e2之前,那么我們說e2發(fā)生在e1之后。 同樣,如果e1不在e2之前發(fā)生并且在e2之后也沒有發(fā)生,那么我們說e1和e2同時發(fā)生。
Happen-Before是一個術(shù)語,不僅僅是go才有的。
原始的定義是由Leslie Lamport在1978年的文章 Time, Clocks and the Ordering of Events in a Distributed System中提出來的
在單個goroutine中,happens-before發(fā)生順序即是程序中的順序。
如果同時滿足下述兩個規(guī)則,那么對變量v的讀操作r能夠觀察到對變量v的寫操作w:
讀操作r沒有發(fā)生在寫操作w之前
沒有另外一個針對變量v的寫操作w`發(fā)生在,寫操作w之后且在讀操作r之前
為了保證對變量v的讀操作r能夠觀察到對變量v的特定的寫操作w的值,即確保讀操作r可以觀察到寫操作w是唯一的對變量v的寫操作。 那么,如果滿足下述兩個原則,讀操作r將能夠觀察到寫操作w
w發(fā)生在r之前
任何其他的對共享變量v的寫操作w`發(fā)生在寫操作w之前或者讀操作r之后
這對條件的約束強于前面一對條件的約束;它需要沒有其他任何的寫操作為w`,與寫操作w 或 讀操作r同時發(fā)生。
在單一的goroutine中,由于沒有并發(fā),因此兩個定義也是等價的:讀操作r可以觀察到最近的寫操作w寫入到v的值。當多個goroutine訪問一個共享變量v時,必須使用同步事件建立happens-before條件來確保讀操作觀察到期望的寫操作的變化。
使用變量v的類型零值初始化變量v的行為與內(nèi)存模型中寫操作相同。
如果讀取和寫入的值大于一個機器字,那么多個機器字節(jié)的操作順序不是固定的順序。
同步
初始化
程序初始化操作是在單個goroutine中,但是該goroutine可能會創(chuàng)建的其他goroutine,這些goroutines是并發(fā)運行的。
如果包p導(dǎo)入了包q,則q包中的的init函數(shù)的完成發(fā)生在任何p包中的init函數(shù)開始之前。
函數(shù)main.main的啟動發(fā)生在所有init函數(shù)完成之后。
Goroutine創(chuàng)建
使用go語句啟動新的goroutine發(fā)生在此goroutine執(zhí)行前。
例如下面的程序
var a string
func f() {
print(a)
}
func hello() {
a = "hello, world"
go f()
}
調(diào)用hello()函數(shù)會在某個時刻打印hello, world字符串(也可能打印字符串的時刻發(fā)生在hello函數(shù)返回之后)
Goroutine銷毀
不能保證goroutine的退出在程序中發(fā)生任何事件之前發(fā)生。 例如下面的程序中:
var a string
func hello() {
go func() { a = "hello" }()
print(a)
}
對變量a的賦值沒有跟隨任何同步事件之后, 所以不能保證會被其他goroutine觀察到。 事實上,一個激進的編譯器可能會刪除整個go聲明語句。
如果想讓一個goroutine的執(zhí)行操作影響必須對另外一個goroutine可見, 可以使用同步機制(例如lock或者channel通信)來確定相對的順序。
Channel 通信
channel通信是goroutine之間同步數(shù)據(jù)的主要方法。 通常在不同的goroutine中,將特定channel上的每個發(fā)送與該channel上的相應(yīng)接收進行匹配。
channel上的發(fā)送 happens-before 該channel上的相應(yīng)接收操作完成。
下面程序
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信息的發(fā)送,c的發(fā)送一定會先于c接受的完成,因此這些都先于print調(diào)用完成
happens before 具有傳遞性
關(guān)閉一個channel happens-before channel收到返回零值, 因為channel已經(jīng)關(guān)閉了
在前面的例子中,如果使用close(c)代替c <- 0,能夠保證相同的行為。
從一個無緩沖的channel接收數(shù)據(jù) happens-before 向這個channel完成發(fā)送數(shù)據(jù)之前
下面這段程序(如前面的代碼,但是使用無緩沖的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的寫操作發(fā)生在channel變量c的接收之前, c的接收發(fā)生在c的發(fā)送之前,c的發(fā)送發(fā)生在print打印之前。
如果是帶緩沖的channel(例如c = make(chan int, 1)),那么程序則不一定能保證會打印“hello,world”。(可能打印空字符串或者crash、或者其他未知情況)
在緩沖大小為C的chanel上接收第k個數(shù)據(jù) happens-before 在第 k + C發(fā)送完成之前。
該規(guī)則將前一個規(guī)則(無緩沖的channel)推廣到緩沖channel。 它允許通過緩沖的channel對計數(shù)信號量進行建模:channel中的項目數(shù)量對應(yīng)于活動使用的數(shù)量,channel的容量對應(yīng)于同時使用的最大數(shù)量,發(fā)送一個項目獲取信號量,以及 接收項目會釋放信號量。 這是限制并發(fā)性的常見用法。
該程序為工作列表中的每個條目啟動一個goroutine,但是goroutine使用限制channel進行協(xié)調(diào),以確保一次最多運行三個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包實現(xiàn)了兩種類型的鎖:互斥鎖(sync.Mutex)及讀寫鎖(sync.RWMutex)
對于任意 sync.Mutex 或 sync.RWMutex 類型的變量l。 如果 n < m ,那么第n次 l.Unlock() 調(diào)用在第 m次 l.Lock()調(diào)用返回前發(fā)生。
下面程序
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"。第一次調(diào)用l.Unlock()(在函數(shù)f中)發(fā)生在第二次調(diào)用l.Lock()之前(在main函數(shù)中),第二次調(diào)用l.Lock()發(fā)生在print之前。
上面的例子相當于 n=1, m=2
在一個sync.RWMutex變量l上任何調(diào)用 l.RLock,都有一個n,使得l.RLock在調(diào)用n個l.Unlock之后發(fā)生(并返回),并且匹配的l.RUnlock發(fā)生在調(diào)用第n + 1個l.Lock之前。
Once
這個同步包為多個goroutines進行初始化只執(zhí)行一次的操作,提供了一種安全的機制,多個線程可以執(zhí)行once.Do(f),那么函數(shù)會且只會有一個執(zhí)行f函數(shù),而其他調(diào)用將阻塞直到f函數(shù)的返回。
使用once.Do(f)執(zhí)行f()并返回,發(fā)生在任何調(diào)用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()
}
調(diào)用twoprint將只調(diào)用一次setup函數(shù)。 setup函數(shù)將在兩次調(diào)用打印之前完成。 結(jié)果將是“ hello,world”將被打印兩次。
錯誤的同步
注意,讀操作r可能會觀察到與r同時發(fā)生的寫操作w寫入的值。 即使發(fā)生這種情況,也并不意味著在r之后發(fā)生的讀取將觀察到在w之前發(fā)生的寫入。
var a, b int
func f() {
a = 1
b = 2
}
func g() {
print(b)
print(a)
}
func main() {
go f()
g()
}
有可能打印出2和0
即b已經(jīng)賦值為2,但是a還未被賦值
這個事實使一些常見的習(xí)語無效。
雙檢鎖是為了避免同步的開銷,例如,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函數(shù)里面,能夠觀察到變量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)
}
上述示例所示,在主函數(shù)main中,觀察到done值為true并不能保證一定能看到對a的賦值變化, 因此本示例程序也有可能打印空值。 更糟糕的是,setup函數(shù)對done=true的賦值,并不一定能被main函數(shù)觀察到,因此這兩個線程之間并沒有使用任何的同步事件。 因此for循環(huán)在主函數(shù)中并不能保證一定會循環(huán)結(jié)束。
下述為一些更為難以發(fā)現(xiàn)的變體
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)
}
即使主函數(shù)觀察到g != nil且退出了循環(huán),也不能保證main能夠觀察到g.msg的賦值內(nèi)容。
對于上述的所有錯誤示例,都可以采用同樣的解決辦法:使用明確的同步方式。