併發 Go 程式中的共享變數 (三):讀寫鎖
本系列是閱讀 “The Go Programming Language” 理解和記錄。
在上篇中ofollow,noindex" target="_blank">併發 Go 程式中的共享變數 (二):鎖 我們的獲取 balance 的方法也用了鎖:
func Balance()int { mu.Lock() defer mu.Unlock() return balance }
這樣帶來什麼問題呢?假設有一個場景,使用者需要頻繁的查詢 balance,這會導致 lock 被頻繁的呼叫,不但Balance
函式彼此之間需要等待對方鎖的釋放,同時也會影響到Widthdraw
和Deposit
函式的呼叫:
package main import ( "fmt" "sync" "time" ) var ( musync.Mutex balance int ) func Balance()int { fmt.Println("Balance wait for another goroutine release lock") mu.Lock() fmt.Println("Balance acquired lock") defer mu.Unlock() return balance } func Balance2()int { mu.Lock() fmt.Println("Balance2 acquired lock") defer mu.Unlock() time.Sleep(10 * time.Second) fmt.Println("Balance2 release lock") return balance } func Deposit(amountint) { mu.Lock() deposit(amount) mu.Unlock() } func Withdraw(amountint)bool { mu.Lock() defer mu.Unlock() deposit(-amount) if balance < 0 { deposit(amount) return false // insufficient funds } return true } func deposit(amountint) { balance = balance + amount } func main() { balance = 100 wait := make(chan int) go func() { fmt.Println("Balance2 == >", Balance2()) wait <- 1 }() time.Sleep(2 * time.Second)// 此處 sleep 操作是為了 Balance2 優先獲得執行 fmt.Println("Balance ==>", Balance()) <-wait }
為了演示方便,程式碼中構造了兩個讀取 balance 的函式Balance
和Balance2
,假設在Balance2
由於某種原因讀取 balance 的操作需要等待一段時間,這個時候如果Balance2
不結束Balance
就無法執行,輸出結果如下:
Balance2 acquired lock Balance wait for another goroutine release lock Balance2 release lock Balance2 == > 100 Balance acquired lock Balance ==> 100
由輸出結果不難看出(程式中的 IO 操作能夠引起 goroutine 執行的切換,所以需要小心對待 print 才能正確演示我們要的結果),Balance 必須要等到 Balance2 釋放鎖之後才能獲取 balance,程式的整個執行過程中並沒有修改 balance 的操作,也就是如果僅僅只有讀取 balance 的操作,它們的併發執行是安全的,但是由於sync.Mutex
的使用,這將導致這種併發安全的操作也會帶來不必要的效能損耗:鎖的頻繁獲取和釋放
。這裡如果有一種特殊的鎖能夠允許對 balance 的讀取操作可以並行執行,但是一旦遇到修改操作就必須要等待鎖的獲取才能繼續讀取,這部分的效能損耗就可以彌補。幸運地是,Go 提供了這種鎖,稱之為multiple readers,single writer
鎖:sync.RWMutex
。
我們隊上面的程式碼進行小小的修改,替換獲取鎖和釋放鎖的程式碼:
var ( musync.RWMutex balance int ) func Balance()int { fmt.Println("Balance wait for another goroutine release lock") mu.RLock() // 修改處 fmt.Println("Balance acquired lock") defer mu.RUnlock()// 修改處 return balance } func Balance2()int { mu.RLock()// 修改處 fmt.Println("Balance2 acquired lock") defer mu.RUnlock()// 修改處 time.Sleep(10 * time.Second) fmt.Println("Balance2 release lock") return balance }
執行輸出如下:
Balance2 acquired lock Balance wait for another goroutine release lock Balance acquired lock Balance ==> 100 Balance2 release lock Balance2 == > 100
可以看到即使 Balance2 沒有釋放鎖,Balance 依然可以獲得鎖,程式整體的執行效率提升了,尤其是讀越多,效果越顯著。
使用了 RLock 的 Balance 依然在遇到 Widthdraw 等已經通過 Lock 獲取鎖的 函式執行時必須要繼續等待:
package main import ( "fmt" "sync" "time" ) var ( musync.RWMutex balance int ) func Balance()int { fmt.Println("Balance wait for another goroutine release lock") mu.RLock() fmt.Println("Balance acquired lock") defer mu.RUnlock() return balance } func Deposit(amountint) { mu.Lock() fmt.Println("Deposit acquired lock") defer mu.Unlock() time.Sleep(5 * time.Second) deposit(amount) } func Withdraw(amountint)bool { mu.Lock() defer mu.Unlock() deposit(-amount) if balance < 0 { deposit(amount) return false // insufficient funds } return true } func deposit(amountint) { balance = balance + amount } func main() { balance = 100 wait := make(chan int) go func() { Deposit(100) wait <- 1 }() time.Sleep(1 * time.Second) fmt.Println("Balance ==>", Balance()) <-wait }
即使使用 RLock,Balance 函式還是需要等待 Deposit 釋放鎖,說明我們的目的達到了:多讀並行,一寫排它 。
sync.RWMutex
提供的 RLock 只能用於 critical section 沒有對 shared variable 進行寫的情況,但是記住要始終謹慎對待,因為有很多隱式的對 shared variable 的修改不是很容易察覺,比如其它呼叫函式的讀取計數器等。