## 9.2. sync.Mutex互斥鎖
在8.6節中,我們使用了一個buffered channel作為一個計數信號量,來保證最多只有20個goroutine會同時執行HTTP請求。同理,我們可以用一個容量只有1的channel來保證最多只有一個goroutine在同一時刻訪問一個共享變量。一個只能為1和0的信號量叫做二元信號量(binary semaphore)。
<u><i>gopl.io/ch9/bank2</i></u>
```go
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
}
```
這種互斥很實用,而且被sync包里的Mutex類型直接支持。它的Lock方法能夠獲取到token(這里叫鎖),并且Unlock方法會釋放這個token:
<u><i>gopl.io/ch9/bank3</i></u>
```go
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余額變量),它都會調用mutex的Lock方法來獲取一個互斥鎖。如果其它的goroutine已經獲得了這個鎖的話,這個操作會被阻塞直到其它goroutine調用了Unlock使該鎖變回可用狀態。mutex會保護共享變量。慣例來說,被mutex所保護的變量是在mutex變量聲明之后立刻聲明的。如果你的做法和慣例不符,確保在文檔里對你的做法進行說明。
在Lock和Unlock之間的代碼段中的內容goroutine可以隨便讀取或者修改,這個代碼段叫做臨界區。鎖的持有者在其他goroutine獲取該鎖之前需要調用Unlock。goroutine在結束后釋放鎖是必要的,無論以哪條路徑通過函數都需要釋放,即使是在錯誤路徑中,也要記得釋放。
上面的bank程序例證了一種通用的并發模式。一系列的導出函數封裝了一個或多個變量,那么訪問這些變量唯一的方式就是通過這些函數來做(或者方法,對于一個對象的變量來說)。每一個函數在一開始就獲取互斥鎖并在最后釋放鎖,從而保證共享變量不會被并發訪問。這種函數、互斥鎖和變量的編排叫作監控monitor(這種老式單詞的monitor是受"monitor goroutine"的術語啟發而來的。兩種用法都是一個代理人保證變量被順序訪問)。
由于在存款和查詢余額函數中的臨界區代碼這么短--只有一行,沒有分支調用--在代碼最后去調用Unlock就顯得更為直截了當。在更復雜的臨界區的應用中,尤其是必須要盡早處理錯誤并返回的情況下,就很難去(靠人)判斷對Lock和Unlock的調用是在所有路徑中都能夠嚴格配對的了。Go語言里的defer簡直就是這種情況下的救星:我們用defer來調用Unlock,臨界區會隱式地延伸到函數作用域的最后,這樣我們就從“總要記得在函數返回之后或者發生錯誤返回時要記得調用一次Unlock”這種狀態中獲得了解放。Go會自動幫我們完成這些事情。
```go
func Balance() int {
mu.Lock()
defer mu.Unlock()
return balance
}
```
上面的例子里Unlock會在return語句讀取完balance的值之后執行,所以Balance函數是并發安全的。這帶來的另一點好處是,我們再也不需要一個本地變量b了。
此外,一個deferred Unlock即使在臨界區發生panic時依然會執行,這對于用recover (§5.10)來恢復的程序來說是很重要的。defer調用只會比顯式地調用Unlock成本高那么一點點,不過卻在很大程度上保證了代碼的整潔性。大多數情況下對于并發程序來說,代碼的整潔性比過度的優化更重要。如果可能的話盡量使用defer來將臨界區擴展到函數的結束。
考慮一下下面的Withdraw函數。成功的時候,它會正確地減掉余額并返回true。但如果銀行記錄資金對交易來說不足,那么取款就會恢復余額,并返回false。
```go
// NOTE: not atomic!
func Withdraw(amount int) bool {
Deposit(-amount)
if Balance() < 0 {
Deposit(amount)
return false // insufficient funds
}
return true
}
```
函數終于給出了正確的結果,但是還有一點討厭的副作用。當過多的取款操作同時執行時,balance可能會瞬時被減到0以下。這可能會引起一個并發的取款被不合邏輯地拒絕。所以如果Bob嘗試買一輛sports car時,Alice可能就沒辦法為她的早咖啡付款了。這里的問題是取款不是一個原子操作:它包含了三個步驟,每一步都需要去獲取并釋放互斥鎖,但任何一次鎖都不會鎖上整個取款流程。
理想情況下,取款應該只在整個操作中獲得一次互斥鎖。下面這樣的嘗試是錯誤的:
```go
// 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會調用mu.Lock()第二次去獲取互斥鎖,但因為mutex已經鎖上了,而無法被重入(譯注:go里沒有重入鎖,關于重入鎖的概念,請參考java)--也就是說沒法對一個已經鎖上的mutex來再次上鎖--這會導致程序死鎖,沒法繼續執行下去,Withdraw會永遠阻塞下去。
關于Go的mutex不能重入這一點我們有很充分的理由。mutex的目的是確保共享變量在程序執行時的關鍵點上能夠保證不變性。不變性的其中之一是“沒有goroutine訪問共享變量”,但實際上這里對于mutex保護的變量來說,不變性還包括其它方面。當一個goroutine獲得了一個互斥鎖時,它會斷定這種不變性能夠被保持。在其獲取并保持鎖期間,可能會去更新共享變量,這樣不變性只是短暫地被破壞。然而當其釋放鎖之后,它必須保證不變性已經恢復原樣。盡管一個可以重入的mutex也可以保證沒有其它的goroutine在訪問共享變量,但這種方式沒法保證這些變量額外的不變性。(譯注:這段翻譯有點暈)
一個通用的解決方案是將一個函數分離為多個函數,比如我們把Deposit分離成兩個:一個不導出的函數deposit,這個函數假設鎖總是會被保持并去做實際的操作,另一個是導出的函數Deposit,這個函數會調用deposit,但在調用前會先去獲取鎖。同理我們可以將Withdraw也表示成這種形式:
```go
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 }
```
當然,這里的存款deposit函數很小,實際上取款Withdraw函數不需要理會對它的調用,盡管如此,這里的表達還是表明了規則。
封裝(§6.6), 用限制一個程序中的意外交互的方式,可以使我們獲得數據結構的不變性。因為某種原因,封裝還幫我們獲得了并發的不變性。當你使用mutex時,確保mutex和其保護的變量沒有被導出(在go里也就是小寫,且不要被大寫字母開頭的函數訪問啦),無論這些變量是包級的變量還是一個struct的字段。
- 前言
- Go語言起源
- Go語言項目
- 本書的組織
- 更多的信息
- 致謝
- 入門
- Hello, World
- 命令行參數
- 查找重復的行
- GIF動畫
- 獲取URL
- 并發獲取多個URL
- Web服務
- 本章要點
- 程序結構
- 命名
- 聲明
- 變量
- 賦值
- 類型
- 包和文件
- 作用域
- 基礎數據類型
- 整型
- 浮點數
- 復數
- 布爾型
- 字符串
- 常量
- 復合數據類型
- 數組
- Slice
- Map
- 結構體
- JSON
- 文本和HTML模板
- 函數
- 函數聲明
- 遞歸
- 多返回值
- 錯誤
- 函數值
- 匿名函數
- 可變參數
- Deferred函數
- Panic異常
- Recover捕獲異常
- 方法
- 方法聲明
- 基于指針對象的方法
- 通過嵌入結構體來擴展類型
- 方法值和方法表達式
- 示例: Bit數組
- 封裝
- 接口
- 接口是合約
- 接口類型
- 實現接口的條件
- flag.Value接口
- 接口值
- sort.Interface接口
- http.Handler接口
- error接口
- 示例: 表達式求值
- 類型斷言
- 基于類型斷言識別錯誤類型
- 通過類型斷言查詢接口
- 類型分支
- 示例: 基于標記的XML解碼
- 補充幾點
- Goroutines和Channels
- Goroutines
- 示例: 并發的Clock服務
- 示例: 并發的Echo服務
- Channels
- 并發的循環
- 示例: 并發的Web爬蟲
- 基于select的多路復用
- 并發的退出
- 示例: 聊天服務
- 基于共享變量的并發
- 競爭條件
- sync.Mutex互斥鎖
- sync.RWMutex讀寫鎖
- 內存同步
- 競爭條件檢測
- 示例: 并發的非阻塞緩存
- Goroutines和線程
- 包和工具
- 包簡介
- 導入路徑
- 包聲明
- 導入聲明
- 包的匿名導入
- 包和命名
- 工具
- 測試
- go test
- 測試函數
- 測試覆蓋率
- 基準測試
- 剖析
- 示例函數
- 反射
- 為何需要反射?
- reflect.Type和reflect.Value
- Display遞歸打印
- 示例: 編碼S表達式
- 通過reflect.Value修改值
- 示例: 解碼S表達式
- 顯示一個類型的方法集
- 幾點忠告
- 底層編程
- unsafe.Sizeof, Alignof 和 Offsetof
- unsafe.Pointer
- 示例: 深度相等判斷
- 通過cgo調用C代碼
- 幾點忠告
- 附錄
- 附錄A:原文勘誤
- 附錄B:作者譯者
- 附錄C:譯文授權
- 附錄D:其它語言