Go 語言 內(nèi)存同步

2023-03-14 16:58 更新

原文鏈接:https://gopl-zh.github.io/ch9/ch9-04.html


9.4. 內(nèi)存同步

你可能比較糾結(jié)為什么Balance方法需要用到互斥條件,無論是基于channel還是基于互斥量。畢竟和存款不一樣,它只由一個(gè)簡單的操作組成,所以不會碰到其它goroutine在其執(zhí)行“期間”執(zhí)行其它邏輯的風(fēng)險(xiǎn)。這里使用mutex有兩方面考慮。第一Balance不會在其它操作比如Withdraw“中間”執(zhí)行。第二(更重要的)是“同步”不僅僅是一堆goroutine執(zhí)行順序的問題,同樣也會涉及到內(nèi)存的問題。

在現(xiàn)代計(jì)算機(jī)中可能會有一堆處理器,每一個(gè)都會有其本地緩存(local cache)。為了效率,對內(nèi)存的寫入一般會在每一個(gè)處理器中緩沖,并在必要時(shí)一起flush到主存。這種情況下這些數(shù)據(jù)可能會以與當(dāng)初goroutine寫入順序不同的順序被提交到主存。像channel通信或者互斥量操作這樣的原語會使處理器將其聚集的寫入flush并commit,這樣goroutine在某個(gè)時(shí)間點(diǎn)上的執(zhí)行結(jié)果才能被其它處理器上運(yùn)行的goroutine得到。

考慮一下下面代碼片段的可能輸出:

var x, y int
go func() {
    x = 1 // A1
    fmt.Print("y:", y, " ") // A2
}()
go func() {
    y = 1                   // B1
    fmt.Print("x:", x, " ") // B2
}()

因?yàn)閮蓚€(gè)goroutine是并發(fā)執(zhí)行,并且訪問共享變量時(shí)也沒有互斥,會有數(shù)據(jù)競爭,所以程序的運(yùn)行結(jié)果沒法預(yù)測的話也請不要驚訝。我們可能希望它能夠打印出下面這四種結(jié)果中的一種,相當(dāng)于幾種不同的交錯(cuò)執(zhí)行時(shí)的情況:

y:0 x:1
x:0 y:1
x:1 y:1
y:1 x:1

第四行可以被解釋為執(zhí)行順序A1,B1,A2,B2或者B1,A1,A2,B2的執(zhí)行結(jié)果。然而實(shí)際運(yùn)行時(shí)還是有些情況讓我們有點(diǎn)驚訝:

x:0 y:0
y:0 x:0

根據(jù)所使用的編譯器,CPU,或者其它很多影響因子,這兩種情況也是有可能發(fā)生的。那么這兩種情況要怎么解釋呢?

在一個(gè)獨(dú)立的goroutine中,每一個(gè)語句的執(zhí)行順序是可以被保證的,也就是說goroutine內(nèi)順序是連貫的。但是在不使用channel且不使用mutex這樣的顯式同步操作時(shí),我們就沒法保證事件在不同的goroutine中看到的執(zhí)行順序是一致的了。盡管goroutine A中一定需要觀察到x=1執(zhí)行成功之后才會去讀取y,但它沒法確保自己觀察得到goroutine B中對y的寫入,所以A還可能會打印出y的一個(gè)舊版的值。

盡管去理解并發(fā)的一種嘗試是去將其運(yùn)行理解為不同goroutine語句的交錯(cuò)執(zhí)行,但看看上面的例子,這已經(jīng)不是現(xiàn)代的編譯器和cpu的工作方式了。因?yàn)橘x值和打印指向不同的變量,編譯器可能會斷定兩條語句的順序不會影響執(zhí)行結(jié)果,并且會交換兩個(gè)語句的執(zhí)行順序。如果兩個(gè)goroutine在不同的CPU上執(zhí)行,每一個(gè)核心有自己的緩存,這樣一個(gè)goroutine的寫入對于其它goroutine的Print,在主存同步之前就是不可見的了。

所有并發(fā)的問題都可以用一致的、簡單的既定的模式來規(guī)避。所以可能的話,將變量限定在goroutine內(nèi)部;如果是多個(gè)goroutine都需要訪問的變量,使用互斥條件來訪問。



以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號