【譯】go記憶體模型
Go記憶體模型制定了一套規則:針對同一個變數,一個goroutine的讀操作如何才能保證(guarantee)觀察到(observe)另一個goroutine的寫操作。
翻譯翻譯,什麼叫觀察到?
有如下程式碼
檢視程式碼
package main import ( "fmt" "runtime" "sync" ) var v string var done bool var wg sync.WaitGroup func write() { v = "hello world" done = true if done { fmt.Println("wrote,", len(v)) } wg.Done() } func read() { for !done { runtime.Gosched() } fmt.Println("read,", v) wg.Done() } func main() { wg.Add(2) go write() go read() wg.Wait() }
如果編譯器沒有重排,輸出結果是:
// write 先行於 read執行 wrote, 11 read, hello world
read讀取到了write後的值,這就是說read觀察到了其他goroutine對變數v的寫,符合我們的預期。
然蛾,假設編譯器在後臺對write進行重排,如下:
func write() { done = true v = "hello world" if done { fmt.Println("wrote,", len(v)) } wg.Done() }
done與v的順序重排了,此時可能的輸出結果有:
// 預期情況:write 先行於 read執行 wrote, 11 read, hello world // 非預期情況:write賦值done後放出cpu -> read排程執行完 -> write排程執行剩下的write v read, wrote, 11
read沒有讀取到write後的值,這就是說read沒有觀察到其他goroutine對變數v的寫,不符合我們的預期。
那要怎麼搞?自己寫的程式碼,編譯器還來插一手,插一手就算了,還插的不對?有什麼好辦法,引出建議。
如果修改資料的同時,有多個goroutine訪問這個資料,那必須要序列化訪問。
要實現序列化訪問,用channel或者sync、async/atomic包裡的同步原語。
如果你必須得讀文件剩下的內容才能理解你自己寫的程式碼,那你就太水了。
做人不要這麼水。
func write() { v = "hello world" if done { v = "hello reorder" } done = true if done { fmt.Println("wrote,", len(v)) } wg.Done() }
這種情況就不能重排v和done,一旦重排,v的值就變了,就會影響這個goroutine的執行結果。
happens before
-- 記憶體操作上的區域性順序。如果事件e1
發生在事件e2
之前(happens before
),那們也可以說e2
發生在e1
之後(happens after
)。如果e1
既不發生在e2
之前,也不發生在e2
之後,那我們就說,e1
、e2
在單個goroutine內,happens before
順序就是程式碼展現的順序。
v
,只有同時滿足以下兩個條件,讀 操作r
才被允許觀察到寫操作w
-
不發生在
w
之前. - 不存在其他的的
w'
發生在w
之後、不在r
關鍵詞:允許(allowed)。
允許,相當於拿到一張抽獎券,至於能不能中獎,後面再說。反正沒有這張券,是必然不能中獎的。
r包含的指令集,每一條都是原子指令:{ read x1; read x2; read v; } w包含的指令集,每一條都是原子指令: { write x1; write x2; write v; }
既然如此,為什麼不直接把1定義為『r發生在w之後』呢?我的理解是,如果直接定義,就相當於直接中獎了,不符合允許這一詞的特性,所以才定義的比較軟。
1的定義有一個漏洞,r是發生在w之後了,但在在w發生後,以可能會有w1、w2、w3等別的寫操作。我要的是r只針對特定的w,不能被其他wx覆蓋。所以增加了2。
w' tov
that happens after w but before r 簡化一下就是:There is no other write that is not before r. 不存在別的寫操作,什麼樣的寫操作?不在r之前的寫操作。 沒有其他的寫,不在r之前。 有其他的寫,在r之前。 所以2的意思是:其他的寫操作發生在w之前 or w同時,並且發生在r之前。 同樣的,對於同時發生的情況,指令集的情況,其他的wx可能會覆蓋w的write。
發生在r
之前;
2. 其他的w'
發生在w
之前,或者r
初始化操作是在單個goroutine內執行,但這個goroutine可能會建立其他的goroutine,導致併發執行。
如果p
包 import q
包,q
包的init
函式 happens before
包的起始程式碼。
main.main
函式 happens after
所有的init
函式完成。
這其實規定了匯入包時,包變數、包init的執行順序。借用beego上的一張圖
var a string
func f() {
print(a)
}
func hello() {
a = "hello, world"
go f()
}
hello
後,在將來某個時間點(也許是在hello退出後)會打印出"hello, world"
var a string
func hello() {
go func() { a = "hello" }()
print(a)
}
var c = make(chan int, 10)
var a string
func f() {
a = "hello, world"
c <- 0 // 傳送, 不重排
}
func main() {
go f()
<-c // 接收
print(a)
}
var c = make(chan int)
var a string
func f() {
a = "hello, world"
<-c
}
func main() {
go f()
c <- 0
print(a)
}
傳送端不重排;因為在講上一個情況時,沒有特意說明channel是緩衝還是不緩衝;
package main import ( "fmt" ) var c = make(chan int, 1) var a string func f() { a = "hello, world" <-c } func main() { go f() c <- 0 fmt.Println(a) }
改成帶緩衝的,首先,channel是帶緩衝,不適合上面的12總結。
要分析的話,得用到下一條規則,總結3:
channel帶緩衝,傳送端與接收端不在同一位置,傳送 happens before 接收;
這就有可能產生非預期情況,列印空字串。
var limit = make(chan int, 3)
func main() {
for _, w := range work {
go func(w func()) {
limit <- 1
w()
<-limit
}(w)
}
select{}
}
接收位置是K,K+C就是發了一圈回到原來K的位置。
如果此時在位置K上,有goroutine阻塞於接收,那麼接收 happens before 傳送完成。
所以,前面對於channel的分情況總結,還需要完善,完整的總結應該是這樣:
channel無緩衝,接收端的goroutine不重排,接收 happens before 傳送;(這點沒改變)
channel帶緩衝,傳送端與接收端在同一位置,接收 happens before 傳送;(補充)
var l sync.Mutex
var a string
func f() {
a = "hello, world"
l.Unlock()
}
func main() {
l.Lock()
go f()
l.Lock()
print(a)
}
Unlock不重排;
Lock與Unlock分開計數;
這,不用翻譯,應該都能懂。
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()
}
var a, b int
func f() {
a = 1
b = 2
}
func g() {
print(b)
print(a)
}
func main() {
go f()
g()
}
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()
}
var a string
var done bool
func setup() {
a = "hello, world"
done = true
}
func main() {
go setup()
for !done {
}
print(a)
}
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)
}
t := new(T) g = t t.msg = "hello, world"