這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
源碼級剖析Go標準庫中的sync.RWMutex。
概述
RWMutex,讀寫鎖,又稱“讀寫互斥鎖”。
讀寫鎖簡單來說就是可以由任意數量的讀者同時使用,或者只由一個寫者使用的鎖。
讀寫鎖和互斥量(Mutex)類似,但是比起互斥量有著更高的並行性,它允許多個讀者同時讀取,因此有一些特殊的應用情境。
在並發編程的很多情境下,資料的讀取可能比寫入更加頻繁,這時就要允許多個線程同時讀取一塊內容。
用例
Go中,RWMutex的零值是一個未加鎖的互斥量。
RWMutex使用起來相對比較簡單,這裡舉一個簡單的例子:
package mainimport ("fmt""sync""time")func main() {rw := new(sync.RWMutex)for i := 0; i < 2; i++ { // 建立兩個寫者go func() {for j := 0; j < 3; j++ {rw.Lock()// 寫rw.Unlock()}}()}for i := 0; i < 5; i++ { // 建立兩個讀者go func() {for j := 0; j < 3; j++ {rw.RLock()// 讀rw.RUnlock()}}()}time.Sleep(time.Second)fmt.Println("Done")}
PlayGround
一個(神奇)優秀的(大坑)特性
讀者在讀的時候,不能夠假定別的讀者也能夠獲得鎖。因此,禁止讀鎖嵌套。
是不是有點兒繞?下面舉個“七秒例”:?
- 第一秒:讀者1在第1秒成功申請了讀鎖
- 第二秒:寫者1在第2秒申請寫鎖,申請失敗,阻塞,但它會防止新的讀者獲鎖
- 第三秒:讀者2在第3秒申請讀鎖,申請失敗
- 第四秒:讀者1釋放讀鎖,寫者1獲得寫鎖
- 第五秒:寫者1釋放寫鎖,讀者2獲得讀鎖
- 第六秒:讀者1再次申請讀鎖,申請成功,與讀者2共用
- 第七秒:讀者1、讀者2釋放讀鎖,結束
當寫鎖阻塞時,新的讀鎖是無法申請的,這可以有效防止寫者饑餓。如果一個線程因為某種原因,導致得不到CPU已耗用時間,這種狀態被稱之為 饑餓。
然而,這種機制也禁止了讀鎖嵌套。讀鎖嵌套可能造成死結:
package mainimport ("fmt""sync""time")func main() {rw := new(sync.RWMutex)var deadLockCase time.Duration = 1go func() {time.Sleep(time.Second * deadLockCase)fmt.Println("Writer Try")rw.Lock()fmt.Println("Writer Fetch")time.Sleep(time.Second * 1)fmt.Println("Writer Release")rw.Unlock()}()fmt.Println("Reader 1 Try")rw.RLock()fmt.Println("Reader 1 Fetch")time.Sleep(time.Second * 2)fmt.Println("Reader 2 Try")rw.RLock()fmt.Println("Reader 2 Fetch")time.Sleep(time.Second * 2)fmt.Println("Reader 1 Release")rw.RUnlock()time.Sleep(time.Second * 1)fmt.Println("Reader 2 Release")rw.RUnlock()time.Sleep(time.Second * 2)fmt.Println("Done")}
讀者1和讀者2是嵌套關係,按照這種時間安排,上述程式會導致死結。
而有些死結的可怕之處就在於,它不一定會發生。假設上面程式中的time.Sleep都是隨機的時間,那麼這一段代碼每次的結果有可能不一致,這會給Debug帶來極大的困難。
吾聞讀鎖莫嵌套,寫鎖嵌套長已矣。(讀鎖嵌套了還有機率成功,寫鎖嵌套了100%完蛋?)
源碼剖析
(源碼具體內容、行數,以版本go version 1.8.1為例。)
為了方便理解,可以把所有的if race.Enabled {...}扔掉不看。接下來,我們重述“七秒例”。?
第一秒,讀者1請求讀鎖。
Line41: if atomic.AddInt32(&rw.readerCount, 1) < 0 {// A writer is pending, wait for it.runtime_Semacquire(&rw.readerSem)}
讀者數量readerCount開始是0,這個時候加1,變成了1,不符合判負條件所以跳出,成功獲得讀鎖一枚。
第二秒,寫者嘗試擷取寫鎖。第85行擷取w的鎖。不管這個讀寫鎖有沒有擷取成功,先排斥別的寫者。
Line85:// First, resolve competition with other writers.rw.w.Lock()// Announce to readers there is a pending writer.r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders// Wait for active readers.if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {runtime_Semacquire(&rw.writerSem)}
剛才說了,一個寫者阻塞在這裡的時候,也不會讓新的讀者去讀了,所以它幹了一件非常壞的事情:
把readerCount變成了1-rwmutexMaxReaders。
這樣就能卡住新來的讀者了。
接下來,算出r等於1。這意味著有當前有寫者存在。
因為有讀者,所以寫者卡在了訊號量writerSem上。但是它不甘心啊,心想“等完現在的這幾個讀者,我就要去寫!”,它默默地把現在佔有讀鎖的人記在了小本本rw.readerWait上。在本例子中,readerWait被設定為了1。
第三秒,讀者2嘗試獲得讀鎖,它又來到了第41行,結果發現讀者的數量是1-rwmutexMaxReaders,好吧,它只好卡在訊號量readerSem上。
第四秒,讀者1調用RUnlock(),它首先把讀者數量減一,畢竟自己已經不讀了。
Line61:if r := atomic.AddInt32(&rw.readerCount, -1); r < 0 {// A writer is pending.if atomic.AddInt32(&rw.readerWait, -1) == 0 {// The last reader unblocks the writer.runtime_Semrelease(&rw.writerSem)}}
在讀者數量減一的時候,它發現讀者數量是負數,這回讀者1明白了,有一個寫者在等待寫。估計讀者1自己已經在這個寫者的小本本readerWait上了,因此它把readerWait減一,表示自己不讀了。這時候讀者1發現自己就是最後一個讀者了,所以趕緊祭出writerSem,讓寫者可以去寫。
讀者1釋放了writerSem訊號量以後,寫者很快就收到了這個提醒,興高采烈地獲得了寫鎖,開始自己的寫作生涯。
讀者2還卡著呢…
第五秒,寫者1寫完了一稿便不想寫了,調用Unlock()準備釋放讀鎖。
Line114:// Announce to readers there is no active writer.r := atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)// Unblock blocked readers, if any.for i := 0; i < int(r); i++ {runtime_Semrelease(&rw.readerSem)}
只見他重新為readerCount加上rwmutexMaxReaders,使他重新變為了正數。這個正數恰好也是阻塞的讀者的數量。
接下來,寫者按照這個讀者的數量,釋放了這麼多的readerSem訊號量,相當於將所有阻塞的讀者一一喚醒。讀者2在收到readerSem的那一刻喜極而泣,它終於可以讀了。
第六秒,讀者1又來了,它把讀者數量加1,發現它是正數哎,寫者現在又沒來,它再次幸運地瞬間獲得讀鎖,與讀者2一起讀了起來。
第七秒,讀者1和讀者2都釋放了自己的讀鎖。至此,結束。
名詞解釋
| 中文 |
英文 |
解釋 |
| 訊號量 (也稱號誌) |
Semaphore |
|
| 條件變數 |
Condition |
|
| 互斥量 |
Mutex |
參考文獻
- Wikipedia: Semaphore (programming))