W3Cschool
恭喜您成為首批注冊(cè)用戶
獲得88經(jīng)驗(yàn)值獎(jiǎng)勵(lì)
原文鏈接:https://gopl-zh.github.io/ch9/ch9-02.html
在8.6節(jié)中,我們使用了一個(gè)buffered channel作為一個(gè)計(jì)數(shù)信號(hào)量,來保證最多只有20個(gè)goroutine會(huì)同時(shí)執(zhí)行HTTP請(qǐng)求。同理,我們可以用一個(gè)容量只有1的channel來保證最多只有一個(gè)goroutine在同一時(shí)刻訪問一個(gè)共享變量。一個(gè)只能為1和0的信號(hào)量叫做二元信號(hào)量(binary semaphore)。
gopl.io/ch9/bank2
var (
sema = make(chan struct{}, 1) // a binary semaphore guarding balance
balance int
)
func Deposit(amount int) {
sema <- struct{}{} // acquire token
balance = balance + amount
<-sema // release token
}
func Balance() int {
sema <- struct{}{} // acquire token
b := balance
<-sema // release token
return b
}
這種互斥很實(shí)用,而且被sync包里的Mutex類型直接支持。它的Lock方法能夠獲取到token(這里叫鎖),并且Unlock方法會(huì)釋放這個(gè)token:
gopl.io/ch9/bank3
import "sync"
var (
mu sync.Mutex // guards balance
balance int
)
func Deposit(amount int) {
mu.Lock()
balance = balance + amount
mu.Unlock()
}
func Balance() int {
mu.Lock()
b := balance
mu.Unlock()
return b
}
每次一個(gè)goroutine訪問bank變量時(shí)(這里只有balance余額變量),它都會(huì)調(diào)用mutex的Lock方法來獲取一個(gè)互斥鎖。如果其它的goroutine已經(jīng)獲得了這個(gè)鎖的話,這個(gè)操作會(huì)被阻塞直到其它goroutine調(diào)用了Unlock使該鎖變回可用狀態(tài)。mutex會(huì)保護(hù)共享變量。慣例來說,被mutex所保護(hù)的變量是在mutex變量聲明之后立刻聲明的。如果你的做法和慣例不符,確保在文檔里對(duì)你的做法進(jìn)行說明。
在Lock和Unlock之間的代碼段中的內(nèi)容goroutine可以隨便讀取或者修改,這個(gè)代碼段叫做臨界區(qū)。鎖的持有者在其他goroutine獲取該鎖之前需要調(diào)用Unlock。goroutine在結(jié)束后釋放鎖是必要的,無論以哪條路徑通過函數(shù)都需要釋放,即使是在錯(cuò)誤路徑中,也要記得釋放。
上面的bank程序例證了一種通用的并發(fā)模式。一系列的導(dǎo)出函數(shù)封裝了一個(gè)或多個(gè)變量,那么訪問這些變量唯一的方式就是通過這些函數(shù)來做(或者方法,對(duì)于一個(gè)對(duì)象的變量來說)。每一個(gè)函數(shù)在一開始就獲取互斥鎖并在最后釋放鎖,從而保證共享變量不會(huì)被并發(fā)訪問。這種函數(shù)、互斥鎖和變量的編排叫作監(jiān)控monitor(這種老式單詞的monitor是受“monitor goroutine”的術(shù)語啟發(fā)而來的。兩種用法都是一個(gè)代理人保證變量被順序訪問)。
由于在存款和查詢余額函數(shù)中的臨界區(qū)代碼這么短——只有一行,沒有分支調(diào)用——在代碼最后去調(diào)用Unlock就顯得更為直截了當(dāng)。在更復(fù)雜的臨界區(qū)的應(yīng)用中,尤其是必須要盡早處理錯(cuò)誤并返回的情況下,就很難去(靠人)判斷對(duì)Lock和Unlock的調(diào)用是在所有路徑中都能夠嚴(yán)格配對(duì)的了。Go語言里的defer簡直就是這種情況下的救星:我們用defer來調(diào)用Unlock,臨界區(qū)會(huì)隱式地延伸到函數(shù)作用域的最后,這樣我們就從“總要記得在函數(shù)返回之后或者發(fā)生錯(cuò)誤返回時(shí)要記得調(diào)用一次Unlock”這種狀態(tài)中獲得了解放。Go會(huì)自動(dòng)幫我們完成這些事情。
func Balance() int {
mu.Lock()
defer mu.Unlock()
return balance
}
上面的例子里Unlock會(huì)在return語句讀取完balance的值之后執(zhí)行,所以Balance函數(shù)是并發(fā)安全的。這帶來的另一點(diǎn)好處是,我們?cè)僖膊恍枰粋€(gè)本地變量b了。
此外,一個(gè)deferred Unlock即使在臨界區(qū)發(fā)生panic時(shí)依然會(huì)執(zhí)行,這對(duì)于用recover(§5.10)來恢復(fù)的程序來說是很重要的。defer調(diào)用只會(huì)比顯式地調(diào)用Unlock成本高那么一點(diǎn)點(diǎn),不過卻在很大程度上保證了代碼的整潔性。大多數(shù)情況下對(duì)于并發(fā)程序來說,代碼的整潔性比過度的優(yōu)化更重要。如果可能的話盡量使用defer來將臨界區(qū)擴(kuò)展到函數(shù)的結(jié)束。
考慮一下下面的Withdraw函數(shù)。成功的時(shí)候,它會(huì)正確地減掉余額并返回true。但如果銀行記錄資金對(duì)交易來說不足,那么取款就會(huì)恢復(fù)余額,并返回false。
// NOTE: not atomic!
func Withdraw(amount int) bool {
Deposit(-amount)
if Balance() < 0 {
Deposit(amount)
return false // insufficient funds
}
return true
}
函數(shù)終于給出了正確的結(jié)果,但是還有一點(diǎn)討厭的副作用。當(dāng)過多的取款操作同時(shí)執(zhí)行時(shí),balance可能會(huì)瞬時(shí)被減到0以下。這可能會(huì)引起一個(gè)并發(fā)的取款被不合邏輯地拒絕。所以如果Bob嘗試買一輛sports car時(shí),Alice可能就沒辦法為她的早咖啡付款了。這里的問題是取款不是一個(gè)原子操作:它包含了三個(gè)步驟,每一步都需要去獲取并釋放互斥鎖,但任何一次鎖都不會(huì)鎖上整個(gè)取款流程。
理想情況下,取款應(yīng)該只在整個(gè)操作中獲得一次互斥鎖。下面這樣的嘗試是錯(cuò)誤的:
// NOTE: incorrect!
func Withdraw(amount int) bool {
mu.Lock()
defer mu.Unlock()
Deposit(-amount)
if Balance() < 0 {
Deposit(amount)
return false // insufficient funds
}
return true
}
上面這個(gè)例子中,Deposit會(huì)調(diào)用mu.Lock()第二次去獲取互斥鎖,但因?yàn)閙utex已經(jīng)鎖上了,而無法被重入(譯注:go里沒有重入鎖,關(guān)于重入鎖的概念,請(qǐng)參考java)——也就是說沒法對(duì)一個(gè)已經(jīng)鎖上的mutex來再次上鎖——這會(huì)導(dǎo)致程序死鎖,沒法繼續(xù)執(zhí)行下去,Withdraw會(huì)永遠(yuǎn)阻塞下去。
關(guān)于Go的mutex不能重入這一點(diǎn)我們有很充分的理由。mutex的目的是確保共享變量在程序執(zhí)行時(shí)的關(guān)鍵點(diǎn)上能夠保證不變性。不變性的一層含義是“沒有g(shù)oroutine訪問共享變量”,但實(shí)際上這里對(duì)于mutex保護(hù)的變量來說,不變性還包含更深層含義:當(dāng)一個(gè)goroutine獲得了一個(gè)互斥鎖時(shí),它能斷定被互斥鎖保護(hù)的變量正處于不變狀態(tài)(譯注:即沒有其他代碼塊正在讀寫共享變量),在其獲取并保持鎖期間,可能會(huì)去更新共享變量,這樣不變性只是短暫地被破壞,然而當(dāng)其釋放鎖之后,鎖必須保證共享變量重獲不變性并且多個(gè)goroutine按順序訪問共享變量。盡管一個(gè)可以重入的mutex也可以保證沒有其它的goroutine在訪問共享變量,但它不具備不變性更深層含義。(譯注:
更詳細(xì)的解釋,Russ Cox認(rèn)為可重入鎖是bug的溫床,是一個(gè)失敗的設(shè)計(jì))
一個(gè)通用的解決方案是將一個(gè)函數(shù)分離為多個(gè)函數(shù),比如我們把Deposit分離成兩個(gè):一個(gè)不導(dǎo)出的函數(shù)deposit,這個(gè)函數(shù)假設(shè)鎖總是會(huì)被保持并去做實(shí)際的操作,另一個(gè)是導(dǎo)出的函數(shù)Deposit,這個(gè)函數(shù)會(huì)調(diào)用deposit,但在調(diào)用前會(huì)先去獲取鎖。同理我們可以將Withdraw也表示成這種形式:
func Withdraw(amount int) bool {
mu.Lock()
defer mu.Unlock()
deposit(-amount)
if balance < 0 {
deposit(amount)
return false // insufficient funds
}
return true
}
func Deposit(amount int) {
mu.Lock()
defer mu.Unlock()
deposit(amount)
}
func Balance() int {
mu.Lock()
defer mu.Unlock()
return balance
}
// This function requires that the lock be held.
func deposit(amount int) { balance += amount }
當(dāng)然,這里的存款deposit函數(shù)很小,實(shí)際上取款Withdraw函數(shù)不需要理會(huì)對(duì)它的調(diào)用,盡管如此,這里的表達(dá)還是表明了規(guī)則。
封裝(§6.6),用限制一個(gè)程序中的意外交互的方式,可以使我們獲得數(shù)據(jù)結(jié)構(gòu)的不變性。因?yàn)槟撤N原因,封裝還幫我們獲得了并發(fā)的不變性。當(dāng)你使用mutex時(shí),確保mutex和其保護(hù)的變量沒有被導(dǎo)出(在go里也就是小寫,且不要被大寫字母開頭的函數(shù)訪問啦),無論這些變量是包級(jí)的變量還是一個(gè)struct的字段。
![]() | ![]() |
Copyright©2021 w3cschool編程獅|閩ICP備15016281號(hào)-3|閩公網(wǎng)安備35020302033924號(hào)
違法和不良信息舉報(bào)電話:173-0602-2364|舉報(bào)郵箱:jubao@eeedong.com
掃描二維碼
下載編程獅App
編程獅公眾號(hào)
聯(lián)系方式:
更多建議: