Go的內(nèi)存模型詳述了"在一個groutine中對變量進行讀操作能夠偵測到在其他goroutine中對該變量的寫操作"的條件.
成都創(chuàng)新互聯(lián)公司聯(lián)系熱線:18982081108,為您提供成都網(wǎng)站建設(shè)網(wǎng)頁設(shè)計及定制高端網(wǎng)站建設(shè)服務,成都創(chuàng)新互聯(lián)公司網(wǎng)頁制作領(lǐng)域10年,包括成都水電改造等多個行業(yè)擁有多年的網(wǎng)站設(shè)計經(jīng)驗,選擇成都創(chuàng)新互聯(lián)公司,為網(wǎng)站保駕護航。Happens Before
對于一個goroutine來說,它其中變量的讀, 寫操作執(zhí)行表現(xiàn)必須和從所寫的代碼得出的預期是一致的。也就是說,在不改變程序表現(xiàn)的情況下,編譯器和處理器為了優(yōu)化代碼可能會改變變量的操作順序即: 指令亂序重排。
但是在兩個不同的goroutine對相同變量操作時, 會因為指令重排導致不同的goroutine對變量的操作順序的認識變得不一致。例如,一個goroutine執(zhí)行a = 1; b = 2;,在另一個goroutine中可能會現(xiàn)感知到變量b先于變量a被改變。
為了解決這種二義性問題,Go語言中引進一個happens before的概念,它用于描述對內(nèi)存操作的先后順序問題。如果事件e1 happens before 事件 e2,我們說事件e2 happens after e1。
如果,事件e1 does not happen before 事件 e2,并且 does not happen after e2,我們說事件e1和e2同時發(fā)生。
對于一個單一的goroutine,happens before 的順序和代碼的順序是一致的。
如果能滿足以下的條件,一個對變量v的 “讀事件r” 可以感知到另一個對變量v的 “寫事件w” :
1、“寫事件w” happens before “讀事件r” 。
2、沒有既滿足 happens after w 同時滿主 happens before r 的對變量v的寫事件w。
為了保證讀事件r可以感知對變量v的寫事件,我們首先要確保w是變量v的唯一的寫事件。同時還要滿足以下條件:
1、“寫事件w” happens before “讀事件r”。
2、其他對變量v的訪問必須 happens before “寫事件w” 或者 happens after “讀事件r”。
第二組條件比第一組條件更加嚴格。因為,它要求在w和 r并行執(zhí)行的程序中不能再有其他的讀操作。
對于在單一的goroutine中兩組條件是等價的,讀事件可以確保感知到對變量的寫事件。但是,對于在 兩個goroutines共享變量v,我們必須通過同步事件來保證 happens-before 條件 (這是讀事件感知寫事件的必要條件)。
將變量v自動初始化為零也是屬于這個內(nèi)存操作模型。
讀寫超過一個機器字長度的數(shù)據(jù),順序也是不能保證的。
同步(Synchronization)
初始化
程序的初始化在一個獨立的goroutine中執(zhí)行。在初始化過程中創(chuàng)建的goroutine將在 第一個用于初始化goroutine執(zhí)行完成后啟動。
如果包p導入了包q,包q的init 初始化函數(shù)將在包p的初始化之前執(zhí)行。
程序的入口函數(shù) main.main 則是在所有的 init 函數(shù)執(zhí)行完成之后啟動。
在任意init函數(shù)中新創(chuàng)建的goroutines,將在所有的init 函數(shù)完成后執(zhí)行。
Goroutine的創(chuàng)建
用于啟動goroutine的go語句在goroutine之前運行。
例如,下面的程序:
var a string; func f() { print(a); } func hello() { a = "hello, world"; go f(); }
調(diào)用hello函數(shù),會在某個時刻打印“hello, world”(有可能是在hello函數(shù)返回之后)。
Channel communication 管道通信
用管道通信是兩個goroutines之間同步的主要方法。通常的用法是不同的goroutines對同一個管道進行讀寫操作,一個goroutines寫入到管道中,另一個goroutines從管道中讀數(shù)據(jù)。
管道上的發(fā)送操作發(fā)生在管道的接收完成之前(happens before)。
例如這個程序:
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的賦值發(fā)生在向管道 c發(fā)送數(shù)據(jù)之前,而管道的發(fā)送操作在管道接收完成之前發(fā)生。因此,在print 的時候,a已經(jīng)被賦值。
從一個unbuffered管道接收數(shù)據(jù)在向管道發(fā)送數(shù)據(jù)完成之前發(fā)送。
下面的是示例程序:
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的賦值在從管道接收數(shù)據(jù) 前發(fā)生,而從管道接收數(shù)據(jù)操作在向unbuffered 管道發(fā)送完成之前發(fā)生。所以,在print 的時候,a已經(jīng)被賦值。
如果用的是緩沖管道(如 c = make(chan int, 1) ),將不能保證輸出 “hello, world”結(jié)果(可能會是空字符串,但肯定不會是他未知的字符串, 或?qū)е鲁绦虮罎ⅲ?/p>
鎖
包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”結(jié)果。因為,第一次 l.Unlock() 調(diào)用(在f函數(shù)中)在第二次 l.Lock() 調(diào)用(在main 函數(shù)中)返回之前發(fā)生,也就是在 print 函數(shù)調(diào)用之前發(fā)生。
For any call to l.RLock on a sync.RWMutex variable l, there is an n such that the l.RLock happens (returns) after the n'th call to l.Unlock and the matching l.RUnlock happens before the n+1'th call to l.Lock.
Once
包once提供了一個在多個goroutines中進行初始化的方法。多個goroutines可以 通過 once.Do(f) 方式調(diào)用f函數(shù)。但是,f函數(shù) 只會被執(zhí)行一次,其他的調(diào)用將被阻塞直到唯一執(zhí)行的f()返回。once.Do(f) 中唯一執(zhí)行的f()發(fā)生在所有的 once.Do(f) 返回之前。
有代碼:
var a string func setup() { a = "hello, world"; } func doprint() { once.Do(setup); print(a); } func twoprint() { go doprint(); go doprint(); }
調(diào)用twoprint會輸出“hello, world”兩次。第一次twoprint 函數(shù)會運行setup唯一一次。
錯誤的同步方式
注意:變量讀操作雖然可以偵測到變量的寫操作,但是并不能保證對變量的讀操作就一定發(fā)生在寫操作之后。
例如:
var a, b int func f() { a = 1; b = 2; } func g() { print(b); print(a); } func main() { go f(); g(); }
函數(shù)g可能輸出2,也可能輸出0。
這種情形使得我們必須回避一些看似合理的用法。
這里用Double-checked locking的方法來代替同步。在例子中,twoprint函數(shù)可能得到錯誤的值:
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暗示已經(jīng)給a賦值了,但是沒有辦法給出保證這一點,所以函數(shù)可能輸出空的值。
另一個錯誤陷阱是忙等待:
var a string var done bool func setup() { a = "hello, world"; done = true; } func main() { go setup(); for !done { } print(a); }
我們沒有辦法保證在main中看到了done值被修改的同時也 能看到a被修改,因此程序可能輸出空字符串。更壞的結(jié)果是,main 函數(shù)可能永遠不知道done被修改,因為在兩個線程之間沒有同步操作,這樣main 函數(shù)永遠不能返回。
下面的用法本質(zhì)上也是同樣的問題.
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 條件并且退出了循環(huán),但是任何然 不能保證它看到了g.msg的初始化之后的結(jié)果。
以上就是Go語言的內(nèi)存模型介紹的詳細內(nèi)容,更多請關(guān)注創(chuàng)新互聯(lián)成都網(wǎng)站設(shè)計公司其它相關(guān)文章!
另外有需要云服務器可以了解下創(chuàng)新互聯(lián)scvps.cn,海內(nèi)外云服務器15元起步,三天無理由+7*72小時售后在線,公司持有idc許可證,提供“云服務器、裸金屬服務器、高防服務器、香港服務器、美國服務器、虛擬主機、免備案服務器”等云主機租用服務以及企業(yè)上云的綜合解決方案,具有“安全穩(wěn)定、簡單易用、服務可用性高、性價比高”等特點與優(yōu)勢,專為企業(yè)上云打造定制,能夠滿足用戶豐富、多元化的應用場景需求。