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