『每周譯Go』更新Go內(nèi)存模型
這是Russ Cox的系列論文的第三篇,也是最后一篇: Updating the Go Memory Model。
文章對 官方的Go內(nèi)存模型做了一些補充和思考。

當(dāng)前的Go語言內(nèi)存模型是在2009年編寫的,從那以后略有更新。很明顯,至少有一些細節(jié)我們應(yīng)該添加到當(dāng)前的內(nèi)存這個內(nèi)存模型中,其中包括對競態(tài)檢測器的明確認可,以及關(guān)于sync/atomic中的API是如何同步程序的清晰聲明。
這篇文章重申了Go的總體哲學(xué)和當(dāng)前的內(nèi)存模型,然后概述了我認為我們應(yīng)該對Go內(nèi)存模型進行的相對較小的調(diào)整。假定你已經(jīng)了解了前兩篇文章“硬件內(nèi)存模型”和“編程語言內(nèi)存模型”中的背景知識。
我已經(jīng)開啟了一個GitHub討論項目來收集對反饋。根據(jù)這些反饋,我打算在本月晚些時候準備一份正式的Go提案。使用GitHub討論本身就是一個實驗,我還會繼續(xù)嘗試找到一個合理的方法來擴大這些重要變化的討論。
Go 設(shè)計哲學(xué)
Go旨在成為構(gòu)建實用、高效系統(tǒng)的編程環(huán)境。它的目標(biāo)是為小型項目的輕量級開發(fā)語言,但也可以優(yōu)雅地擴展到大型項目和大型工程團隊。
Go鼓勵在高層次上處理并發(fā),特別是通過通信。第一句Go箴言(Go proverb)就是“不要通過共享內(nèi)存來通信,而是通過通信共享內(nèi)存。”另一個流行的諺語是“清晰勝于聰明。”換句話說,Go鼓勵通過避免使用巧妙的代碼來避免狡猾的bug。
Go的目標(biāo)不僅僅是可以理解的程序,還包括可以理解的語言和可以理解的package API。復(fù)雜或巧妙的語言特征或API與這一目標(biāo)相矛盾。正如Tony Hoare在1980年圖靈獎演講中所說:
I conclude that there are two ways of constructing a software design: One way is to make it so simple that there are obviously no deficiencies and the other way is to make it so complicated that there are no obvious deficiencies.
我的結(jié)論是,構(gòu)建軟件設(shè)計有兩種方法:一種方法是簡單實現(xiàn),以至于明顯沒有缺陷;另一種方法是異常復(fù)雜,以至于沒有明顯缺陷。
第一種方法要困難得多。它需要同樣的技巧、奉獻、洞察力,甚至是靈感,就像發(fā)現(xiàn)構(gòu)成復(fù)雜自然現(xiàn)象基礎(chǔ)的簡單物理定律一樣。它還要求愿意接受受物理、邏輯和技術(shù)限制的目標(biāo),并在沖突的目標(biāo)無法實現(xiàn)時接受妥協(xié)。
這與Go的設(shè)計API的理念非常吻合。我們通常在設(shè)計過程中花很長時間來確保一個應(yīng)用編程接口是正確的,并努力將其簡化為最基本、最有用的精華。
Go作為一個有用的編程環(huán)境的另一方面是為最常見編程錯誤有定義明確的語義,這有助于理解和調(diào)試。這個想法并不新鮮。再次引用Tony Hoare的話,這是來自他1972年的“軟件質(zhì)量”檢查單:
As well as being very simple to use, a software program must be very difficult to misuse; it must be kind to programming errors, giving clear indication of their occurrence, and never becoming unpredictable in its effects.
一個軟件程序不僅使用起來非常簡單,而且很難被誤用;它必須友好對待編程錯誤,給出它們發(fā)生的明確指示,并且其影響永遠不會變得不可預(yù)測。
為有問題的程序定義良好的語義,這種常識并不像人們預(yù)期的那樣普遍。在C/C++中,未定義的行為已經(jīng)演變成一種編譯器作者的全權(quán)委托,以越來越有趣的方式將有輕微問題的程序轉(zhuǎn)換成有大問題的程序。Go不采用這種方法:不存在“未定義的行為”。特別是,像空指針取消引用、整數(shù)溢出和無意的無限循環(huán)這樣的錯誤都在Go中定義了語義。
當(dāng)前的 Go內(nèi)存模型
Go的內(nèi)存模型始于以下建議,符合Go的總體哲學(xué):
修改由多個goroutines同時訪問的數(shù)據(jù)的程序必須串行化這些訪問。 為了實現(xiàn)串行訪問, 需要使用channel操作或其他同步原語(如sync和sync/atomic包中的原語)來保護數(shù)據(jù)。 如果你必須閱讀本文的其余部分才能理解你的程序的行為,那你太聰明了。 別自作聰明。這仍然是個好建議。該建議也與其他語言對DRF-SC的鼓勵使用一致:同步以消除數(shù)據(jù)競爭,然后程序?qū)⒈憩F(xiàn)得好像順序一致,不需要理解內(nèi)存模型的其余部分。
根據(jù)這個建議,Go內(nèi)存模型定義了一個傳統(tǒng)的基于happens-before對讀寫競爭的定義。像在Java和JavaScript中一樣,在Go中的讀操作可以觀察到任何更早但尚未被覆蓋的寫操作,或者任何競爭的寫操作;僅安排一個這樣的寫入會強制產(chǎn)生指定的結(jié)果。
然后,內(nèi)存模型繼續(xù)定義同步操作,這些操作建立交替執(zhí)行的goroutine的happen-before關(guān)系。操作盡管稀松平常,但是還是帶有一些Go特有的風(fēng)格:
如果package p引入了packageq,那么q的init函數(shù)的執(zhí)行完成一定happen-beforep的所有init函數(shù)(之前)main.main函數(shù)一定 happen after 所有的init函數(shù)完成(之后)go語句創(chuàng)建一個goroutine一定happen before goroutine執(zhí)行(之前) 往一個channel中send happen before 從這個channel receive這個數(shù)據(jù)完成(之前) 一個channel的close一定happen before 從這個channel receive到零值數(shù)據(jù)(這里指因為close而返回的零值數(shù)據(jù)) 從一個unbuffered channel的receive一定happen before 往這個channel send完成(之前) 從容量為C的channel receive第k個數(shù)據(jù)一定happen before第k+C次send完成(之前) 對于任意的 sync.Mutex或者sync.RWMutex類型的變量l以及n < m, 調(diào)用第n次l.UnLock()一定happen before 第m次的l.Lock()返回(之前)once.Do(f)中的對f的單次調(diào)用一定happen before 任意次的對once.Do(f)調(diào)用返回(之前) 值得注意的是,這個列表忽略了package sync中新加的API以及sync/atomic的API。
Go內(nèi)存模型規(guī)范以一些不正確同步的例子結(jié)束。它沒有包含錯誤編譯的例子。
對Go內(nèi)存模型做的改變
2009年,當(dāng)我們著手編寫Go的內(nèi)存模型時,Java內(nèi)存模型進行了新的修訂,C/C++11內(nèi)存模型正在定稿。一些人強烈鼓勵我們采用C/C++11模型,并充分利用了其已經(jīng)完成的所有工作。對我們來說這似乎很冒險。相反,我們決定采用一種更保守的方法來保證我們要做的,這一決定得到了隨后十年詳細描述Java/C/C++內(nèi)存模型中非常狡猾問題的論文的證實。是的,定義足夠充分的內(nèi)存模型來指導(dǎo)程序員和編譯器作者是很重要的,但是完全正式地定義一個正確的內(nèi)存模型似乎仍然超出了最有才華的研究人員的能力范圍。Go定義一個最小的需求就足夠了。
下面這一部分列出了我認為我們應(yīng)該做的調(diào)整。如前所述,我已經(jīng)開啟了一個GitHub討論項來收集反饋。根據(jù)這些反饋,我計劃在本月晚些時候準備一份正式的Go提案。
1.文檔化Go的整體方法
“不要聰明”的建議很重要,應(yīng)該堅持下去,但我們也需要在深入研究happen before細節(jié)之前,對Go的整體方法更多的談一談。我看到過很多關(guān)于Go方法的不正確總結(jié),比如宣稱Go的模型是C/C++的“DRF-SC或Catch Fire”。這種誤會是可以理解的: Go內(nèi)存模型規(guī)范沒有說它的方法是什么,而且它是如此之短(材料又如此微妙),以至于人們看到了他們期望看到的東西,而不是那里有什么或沒有什么。
擬在Go內(nèi)存模型規(guī)范中增加的文檔大致如下:
概觀
Go以與本語言其余部分幾乎相同的方式處理其內(nèi)存模型,旨在保持語義簡單、可理解和有用。
數(shù)據(jù)競爭被定義為對存儲器位置的寫入與對同一位置的另一次讀取或?qū)懭胪瑫r發(fā)生,除非所有訪問都是由sync/atomic package提供的原子數(shù)據(jù)訪問提供。如前所述,強烈建議程序員使用適當(dāng)?shù)耐絹肀苊鈹?shù)據(jù)競爭。在沒有數(shù)據(jù)競爭的情況下,Go程序表現(xiàn)得好像所有的gorouitine都被多路復(fù)用到一個處理器上。這個屬性有時被稱為DRF-SC:無數(shù)據(jù)競爭的程序以順序一致的方式執(zhí)行。
其他編程語言通常采用兩種方法之一來處理包含數(shù)據(jù)競爭的程序。第一,以C和C++為例,帶有數(shù)據(jù)競爭的程序是無效的:編譯器可能會以任意令人驚訝的方式中斷它們。第二,以Java和JavaScript為例,具有數(shù)據(jù)競爭的程序定義了語義,通過限制競爭的可能影響,使程序更加可靠和易于調(diào)試。Go的方法介于這兩者之間。具有數(shù)據(jù)競爭的程序是無效的,因為語言實現(xiàn)可能會報告競爭并終止程序。但另一方面,具有數(shù)據(jù)競爭的程序定義了具有有限數(shù)量結(jié)果的語義,使得錯誤的程序更可靠,更容易調(diào)試。
這些文字應(yīng)該闡明Go和其他語言有什么不同,糾正讀者先前的任何期望。
在“happen before”一節(jié)的最后,我們還應(yīng)該澄清某些競爭仍然會導(dǎo)致內(nèi)存損壞。當(dāng)前它以下面的句子結(jié)束:
Reads and writes of values larger than a single machine word behave as multiple machine-word-sized operations in an unspecified order.
我們應(yīng)該加上一點:
請注意,這意味著多word數(shù)據(jù)結(jié)構(gòu)上的競爭可能導(dǎo)致單次寫入產(chǎn)生不一致值。當(dāng)值依賴于內(nèi)部(指針、長度)或(指針、類型)pair的一致性時,就像大多數(shù)Go實現(xiàn)中的接口、map、切片和字符串的情況一樣,這種競爭又會導(dǎo)致內(nèi)存損壞。
這將更清楚地說明保證對具有數(shù)據(jù)競爭的程序的限制。
2.文檔化 sync庫的happen before
自從Go內(nèi)存模型發(fā)布以來,一些新的API已經(jīng)被添加到sync包中。我們需要將它們添加到內(nèi)存模型中(issue#7948)。謝天謝地謝廣坤,增加的內(nèi)容看起來很簡單。我相信它們應(yīng)該如下:
對于sync.Cond, Broadcast和Signal一定happen before 它解鎖的Wait方法調(diào)用完成(之前)對于sync.Map, Load, LoadAndDelete 和 LoadOrStore 都是讀操作, Delete、LoadAndDelete和 Store都是寫操作。LoadOrStore當(dāng)它的loaded返回false時是寫操作。一個寫操作happen before 能觀察到這個寫操作的讀操作(之前) 對于sync.Pool,對Put(x)的調(diào)用一定happen before Get方法返回這個x(之前)。同樣的,返回x的New方法一定happen before Get方法返回這個x(之前) 對于sync.EWaitGroup, Done方法的調(diào)用一定happen before 它解鎖的Wait方法調(diào)用返回(之前) 這些API的用戶需要知道保證,以便有效地使用它們。因此,雖然我們應(yīng)該將這些文字保留在內(nèi)存模型中以供介紹,但我們也應(yīng)該將其包含在package sync的文檔注釋中。這也將有助于為第三方同步原語樹立一個榜樣,說明記錄由API建立的順序保證的重要性。
3.文檔話 sync/atomic的happen before
Atomic operations are missing from the memory model. We need to add them (issue #5045). I believe we should say:
內(nèi)存模型中缺少原子操作的保證。我們需要添加它們(issue #5045)。我認為我們應(yīng)該說:
sync/atomic package中的API統(tǒng)稱為“原子操作”,可用于同步各種goroutine執(zhí)行。如果原子操作A的效果被原子操作B觀察到,那么A發(fā)生在B之前(happen before)。在一個程序中執(zhí)行的所有原子操作表現(xiàn)得好像是以某種順序一致的順序執(zhí)行的。
這是Dmitri Vyukov在2013年提出的建議,也是我在2016年非正式承諾的。它還與Java的volatiles和C++的默認原子具有相同的語義。
就C/C++而言,同步原子只有兩種選擇:順序一致或acquire/release(Relaxed原子不會創(chuàng)建happen before,因此沒有同步效果). 對這兩者的決策歸結(jié)為,第一,能夠推理出多個位置上原子操作的相對順序有多重要,第二,順序一致的原子與acquire/release原子相比要多昂貴(慢)。
首先要考慮的是,關(guān)于多個位置上原子操作的相對順序的推理非常重要。在之前的一篇文章中,我舉了一個使用兩個原子變量實現(xiàn)的無鎖快速路徑的條件變量的例子,這兩個原子變量被使用acuqire/release原子打破了。這種模式反復(fù)出現(xiàn)。例如,sync.WaitGroup曾經(jīng)的實現(xiàn)使用了一對原子uint32值:wg.counter和wg.waiters。Go運行時中的信號量的實現(xiàn)也依賴于兩個獨立的原子word,即信號量值*addr和相應(yīng)的waiter count root.nwait。還有更多。在缺乏順序一致的語義的情況下(也就是說,如果我們改為采用acquire/release語義),人們?nèi)匀粫襁@樣錯誤地編寫代碼;它會神秘地失敗,而且只在特定的情況下。
根本的問題是,使用acuqire/release原子使無數(shù)據(jù)競爭的程序不會導(dǎo)致程序以順序一致的方式運行,因為原子本身不會提供保證。也就是說,這樣的程序不提供DRF-SC。這使得這種程序很難推理,因此很難正確編寫。
關(guān)于第二個考慮,正如在之前的文章中提到的,硬件設(shè)計人員開始為順序一致的原子提供直接支持。例如,ARMv8添加了ldar和stlr指令來實現(xiàn)順序一致的原子,它們也是acquire/release原子的推薦實現(xiàn)。如果我們?yōu)閟ync/atomic采用acquire/release語義,那么寫在ARMv8上的程序無論如何都會獲得順序一致性。這無疑會導(dǎo)致依賴更強順序的程序意外地在更弱的平臺上崩潰。,如果由于競爭窗口很小, acquire/release和結(jié)果一致的原子之間的差異在實踐中很難觀察到,這甚至可能發(fā)生在單個架構(gòu)上。
這兩種考慮都強烈建議我們應(yīng)該采用順序一致的原子而不是acquire/release原子:順序一致的原子更有用,一些芯片已經(jīng)完全縮小了這兩個級別之間的差距。如果差距很大,想必其他人也會這么做。
同樣的考慮,以及Go擁有小型、易于理解的API的總體哲學(xué),所有這一切都反對將acuqire/release作為一套額外的并行API來提供。似乎最好只提供最容易理解的,最有用的,很難被誤用的原子操作。
另一種可能性是提供原始屏障,而不是原子操作(當(dāng)然,C++兩者都提供)。屏障的缺點是使期望變得不那么清晰,并且在某種程度上更加局限于特定的體系結(jié)構(gòu)。Hans Boehm文章“Why atomics have integrated ordering constraints”給出了提供原子而不是屏障的論點(他使用術(shù)語柵欄fence)。一般來說,原子比柵欄更容易理解,而且由于我們現(xiàn)在已經(jīng)提供了原子操作,所以我們不能輕易移除它們。一個機制要比提供兩個好。
4.可能的改變:為sync/atomic提供類型化的API
上面的定義說,當(dāng)一個特定的內(nèi)存塊必須由多個線程同時訪問而沒有其他同步時,消除爭用的唯一方法是讓所有的訪問都使用原子。僅僅讓一些訪問使用原子是不夠的。例如,與原子讀或?qū)懖l(fā)的非原子寫仍然是s數(shù)據(jù)競爭,與非原子讀或?qū)懖l(fā)的原子寫也是數(shù)據(jù)競爭。
因此,一個特定的值是否應(yīng)該用atomic訪問是該值的屬性,而不是特定的訪問。正因為如此,大多數(shù)語言將這些信息放在類型系統(tǒng)中,比如Java的volatile int和C++的atomic。Go當(dāng)前的API沒有,這意味著正確的使用需要仔細標(biāo)注結(jié)構(gòu)或全局變量的哪些字段預(yù)計只能使用原子API來訪問。
譯者按: uber提供了類似的庫uber-go/atomic。
為了提高程序的正確性,我開始認為Go應(yīng)該定義一組類型化的原子值,類似于當(dāng)前的原子值。值:Bool、Int、Uint、Int32、Uint32、Int64、Uint64和Uintptr。像Value一樣,它們也有CompareAndSwap、Load、Store和Swap方法。例如:
type Int32 struct { v int32 }
func (i *Int32) Add(delta int32) int32 {
return AddInt32(&i.v, delta)
}
func (i *Int32) CompareAndSwap(old, new int32) (swapped bool) {
return CompareAndSwapInt32(&i.v, old, new)
}
func (i *Int32) Load() int32 {
return LoadInt32(&i.v)
}
func (i *Int32) Store(v int32) {
return StoreInt32(&i.v, v)
}
func (i *Int32) Swap(new int32) (old int32) {
return SwapInt32(&i.v, new)
}
我將Bool包括在列表中,因為我們在Go標(biāo)準庫中多次用原子整數(shù)構(gòu)造了原子Bool(在未暴露的API中)。顯然是有需要的。
我們還可以利用即將到來的泛型支持,并為原子指針定義一個API,該API是類型化的,并且在其API中沒有包不安全:
type Pointer[T any] struct { v *T }
func (p *Pointer[T]) CompareAndSwap(old, new *T) (swapped bool) {
return CompareAndSwapPointer(... lots of unsafe ...)
}
(以此類推),你可能會想到不能使用泛型定義一個類型嗎?我沒有看到一個干凈的方法使用泛型來實現(xiàn)atomic.Atomic[T],避免我們引入Bool、Int等作為單獨的類型。走走看吧。
5.可能的改變: 增加非同步的atomic
所有其他現(xiàn)代編程語言都提供了一種方法來進行并發(fā)內(nèi)存讀寫,這種方法不會使程序同步,但也不會使程序無效(不會算作數(shù)據(jù)競爭)。C、C++、Rust和Swift都有relaxed原子。Java有VarHandle的“普通”模式。JavaScript對共享內(nèi)存緩沖區(qū)(唯一的共享內(nèi)存)有非原子的訪問權(quán)限。Go沒有辦法做到這一點。或許應(yīng)該有,我不知道。
如果我們想添加非同步的原子讀寫,我們可以向類型化的原子添加UnsyncAdd、UnsyncCompareAndSwap、UnsyncLoad、UnsyncStore和 UnsyncSwap方法。將它們命名為“unsync”避免了一些“relaxed”名稱的問題。首先,有些人用relaxed作為相對的比較,如“acquire/release是比順序一致性更寬松的內(nèi)存順序。”你可以說這不是這個術(shù)語的恰當(dāng)用法,但它確實發(fā)生了。其次,也是更重要的,這些操作的關(guān)鍵細節(jié)不是操作本身的內(nèi)存排序,而是它們對程序其余部分的同步?jīng)]有影響。對于不是內(nèi)存模型專家的人來說,看到UnsyncLoad應(yīng)該清楚沒有同步,而RelaxedLoad可能不會。在人群中喵一眼Unsync也知道它是不安全的。
有了API,真正的問題是到底要不要添加這些。對提供非同步原子的爭論是,它確實對某些數(shù)據(jù)結(jié)構(gòu)中快速路徑的性能有影響。我的總體印象是,它在非x86架構(gòu)上最重要,盡管我沒有數(shù)據(jù)來支持這一點。不提供不同步的原子可以被認為是對那些架構(gòu)的懲罰。
反對提供非同步原子的一個可能的爭論是,在x86上,忽略了潛在的編譯器重組的影響,非同步原子與acquire/release原子是無法區(qū)分的。因此,他們可能會被濫用來編寫只適用于x86的代碼。反駁的理由是,這樣的花招不會通過race檢測器,它實現(xiàn)的是實際的內(nèi)存模型,而不是x86內(nèi)存模型。
由于缺乏證據(jù),我們沒有理由添加這個API。如果有人強烈認為我們應(yīng)該添加它,那么證明這一點的方法是收集兩方面的證據(jù):(1)程序員需要編寫的代碼的普遍適用性,以及(2)使用非同步原子對廣泛使用的系統(tǒng)產(chǎn)生的顯著性能改進。(使用Go以外的語言的程序來顯示這一點是很好的。)
6.文檔化對編譯器優(yōu)化的禁止項
當(dāng)前的內(nèi)存模型最后給出了無效程序的例子。由于內(nèi)存模型是程序員和編譯器作者之間的契約,我們應(yīng)該添加無效編譯器優(yōu)化的例子。例如,我們可以添加:
6.1不正確的編譯
Go內(nèi)存模型和Go程序一樣限制編譯器優(yōu)化。一些在單線程程序中有效的編譯器優(yōu)化在Go程序中是無效。特別是,編譯器不能在無競爭程序中引入數(shù)據(jù)競爭。它不能允許單次讀取觀察到多個值。并且它不能允許一個寫操作寫入多個值。
Not introducing data races into race-free programs means not moving reads or writes out of conditional statements in which they appear. For example, a compiler must not invert the conditional in this program:
不在無競爭程序中引入數(shù)據(jù)競爭意味著不移動出現(xiàn)條件語句的讀或?qū)憽@纾幾g器不得反轉(zhuǎn)該程序中的條件:
i := 0
if cond {
i = *p
}
也就是說,編譯器不能將程序重寫為這個:
i := *p
if !cond {
i = 0
}
如果cond為false,另一個goroutine正在寫*p,那么原始程序是無競爭的,但是重寫的程序包含競爭。
不引入數(shù)據(jù)競爭也意味著不假設(shè)循環(huán)終止。例如,在這個程序中,編譯器不能將對p或q訪問移動到循環(huán)前面:
n := 0
for e := list; e != nil; e = e.next {
n++
}
i := *p
*q = 1
如果列表指向循環(huán)列表,那么原始程序永遠不會訪問p或q,但是重寫的程序會。
不引入數(shù)據(jù)競爭也意味著不假設(shè)被調(diào)用的函數(shù)總是返回或者沒有同步操作。例如,在這個程序中,編譯器不能移動對p或q訪問到函數(shù)調(diào)用之前:
f()
i := *p
*q = 1
如果調(diào)用從未返回,那么原始程序?qū)⒉粫僭L問p或q,但是重寫的程序會。如果調(diào)用包含同步操作,那么原始程序可以建立f和p/q的happen before關(guān)系,但是重寫的程序就破壞了這個關(guān)系。
不允許單次讀取觀察多個值,意味著不從共享內(nèi)存中重新加載局部變量。例如,在這個程序中,編譯器不能扔掉(spill)i,并重新加載它:
i := *p
if i < 0 || i >= len(funcs) {
panic("invalid function index")
}
... complex code ...
// compiler must NOT reload i = *p here
funcs[i]()
如果復(fù)雜的代碼需要許多寄存器,單線程程序的編譯器可以在不保存副本的情況下丟棄i,然后在funcsi之前重新加載i = p。Go編譯器不能,因為p的值可能已經(jīng)更改。(相反,編譯器可能會將i移動到棧上)。
不允許一次寫操作寫入多個值也意味著不使用在寫入之前將本地變量作為臨時存儲寫入的內(nèi)存。例如,編譯器不得在此程序中使用*p作為臨時存儲:
*p = i + *p/2
也就是說,它絕不能把程序改寫成這樣:
*p /= 2
*p += i
如果i和p開始等于2,則原始代碼最終p = 3,但是一個競爭線程只能從p讀取2或3。重寫后的代碼最終p = 1,然后*p = 3,這也允許競爭線程讀取1。
請注意,所有這些優(yōu)化在C/C++編譯器中都是允許的:與C/C++編譯器共享后端的Go編譯器必須注意禁用對Go無效的優(yōu)化。
這些分類和示例涵蓋了最常見的C/C++編譯器優(yōu)化,這些優(yōu)化與為競爭數(shù)據(jù)訪問定義的語義不兼容。他們明確規(guī)定Go和C/C++有不同的要求。
結(jié)論
Go在其內(nèi)存模型中保守的方法很好地服務(wù)了我們,應(yīng)該繼續(xù)下去。然而,有一些早該做的更改,包括定義sync和sync/package package中新API的同步行為。特別是atomic的內(nèi)存模型應(yīng)該被文檔化,其以提供順序一致的行為,這種行為創(chuàng)建了與它們左右的非原子代碼同步的happen before關(guān)系。這與所有其他現(xiàn)代系統(tǒng)語言提供的默認原子相匹配。
也許更新中最獨特的部分是清楚地聲明具有數(shù)據(jù)競爭的程序可能會被停止以報告競爭,但是在其他方面具有明確定義的語義。這約束了程序員和編譯器,它優(yōu)先考慮并發(fā)程序的可調(diào)試性和正確性,而不是編譯器編寫者的便利性。
感謝
這一系列的帖子從我有幸在谷歌工作的一長串工程師的討論和反饋中受益匪淺。我感謝他們。我對任何錯誤或不受歡迎的意見負全部責(zé)任。
