圖解golang裡面的讀寫鎖實現與核心原理分析瞭解程式語言背後設計

1。 基礎築基

1。1 讀寫鎖的特點

讀寫鎖區別與互斥鎖的主要區別就是讀鎖之間是共享的,多個goroutine可以同時加讀鎖,但是寫鎖與寫鎖、寫鎖與讀鎖之間則是互斥的

1。2 寫鎖飢餓問題

因為讀鎖是共享的,所以如果當前已經有讀鎖,那後續goroutine繼續加讀鎖正常情況下是可以加鎖成功,但是如果一直有讀鎖進行加鎖,那嘗試加寫鎖的goroutine則可能會長期獲取不到鎖,這就是因為讀鎖而導致的寫鎖飢餓問題

1。3 基於高低位與等待佇列的實現

圖解golang裡面的讀寫鎖實現與核心原理分析瞭解程式語言背後設計

在說golang之前介紹一種JAVA裡面的實現,在JAVA中ReentrantReadWriteLock實現採用一個state的高低位來進行讀寫鎖的計數,其中高16位儲存讀的計數,低16位儲存寫的計數,並配合一個AQS來實現排隊等待機制,同時AQS中的每個waiter都會有一個status,用來標識自己的狀態

2。 golang的讀寫鎖的實現

2。1 成員變數

圖解golang裡面的讀寫鎖實現與核心原理分析瞭解程式語言背後設計

2。1。1 結構體

type RWMutex struct { w Mutex // held if there are pending writers writerSem uint32 // 用於writer等待讀完成排隊的訊號量 readerSem uint32 // 用於reader等待寫完成排隊的訊號量 readerCount int32 // 讀鎖的計數器 readerWait int32 // 等待讀鎖釋放的數量}

2。1。2 寫鎖計數

讀寫鎖中允許加讀鎖的最大數量是4294967296,在go裡面對寫鎖的計數採用了負值進行,透過遞減最大允許加讀鎖的數量從而進行寫鎖對讀鎖的搶佔

const rwmutexMaxReaders = 1 << 30

2。2 讀鎖實現

2。2。1 讀鎖加鎖邏輯

圖解golang裡面的讀寫鎖實現與核心原理分析瞭解程式語言背後設計

func (rw *RWMutex) RLock() { if race。Enabled { _ = rw。w。state race。Disable() } // 累加reader計數器,如果小於0則表明有writer正在等待 if atomic。AddInt32(&rw。readerCount, 1) < 0 { // 當前有writer正在等待讀鎖,讀鎖就加入排隊 runtime_SemacquireMutex(&rw。readerSem, false) } if race。Enabled { race。Enable() race。Acquire(unsafe。Pointer(&rw。readerSem)) }}

2。2。2 讀鎖釋放邏輯

圖解golang裡面的讀寫鎖實現與核心原理分析瞭解程式語言背後設計

func (rw *RWMutex) RUnlock() { if race。Enabled { _ = rw。w。state race。ReleaseMerge(unsafe。Pointer(&rw。writerSem)) race。Disable() } // 如果小於0,則表明當前有writer正在等待 if r := atomic。AddInt32(&rw。readerCount, -1); r < 0 { if r+1 == 0 || r+1 == -rwmutexMaxReaders { race。Enable() throw(“sync: RUnlock of unlocked RWMutex”) } // 將等待reader的計數減1,證明當前是已經有一個讀的,如果值==0,則進行喚醒等待的 if atomic。AddInt32(&rw。readerWait, -1) == 0 { // The last reader unblocks the writer。 runtime_Semrelease(&rw。writerSem, false) } } if race。Enabled { race。Enable() }}

2。3 寫鎖實現

2。3。1 加寫鎖實現

圖解golang裡面的讀寫鎖實現與核心原理分析瞭解程式語言背後設計

func (rw *RWMutex) Lock() { if race。Enabled { _ = rw。w。state race。Disable() } // 首先獲取mutex鎖,同時多個goroutine只有一個可以進入到下面的邏輯 rw。w。Lock() // 對readerCounter進行進行搶佔,透過遞減rwmutexMaxReaders允許最大讀的數量 // 來實現寫鎖對讀鎖的搶佔 r := atomic。AddInt32(&rw。readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders // 記錄需要等待多少個reader完成,如果發現不為0,則表明當前有reader正在讀取,當前goroutine // 需要進行排隊等待 if r != 0 && atomic。AddInt32(&rw。readerWait, r) != 0 { runtime_SemacquireMutex(&rw。writerSem, false) } if race。Enabled { race。Enable() race。Acquire(unsafe。Pointer(&rw。readerSem)) race。Acquire(unsafe。Pointer(&rw。writerSem)) }}

2。3。2 釋放寫鎖

圖解golang裡面的讀寫鎖實現與核心原理分析瞭解程式語言背後設計

func (rw *RWMutex) Unlock() { if race。Enabled { _ = rw。w。state race。Release(unsafe。Pointer(&rw。readerSem)) race。Disable() } // 將reader計數器復位,上面減去了一個rwmutexMaxReaders現在再重新加回去即可復位 r := atomic。AddInt32(&rw。readerCount, rwmutexMaxReaders) if r >= rwmutexMaxReaders { race。Enable() throw(“sync: Unlock of unlocked RWMutex”) } // 喚醒所有的讀鎖 for i := 0; i < int(r); i++ { runtime_Semrelease(&rw。readerSem, false) } // 釋放mutex rw。w。Unlock() if race。Enabled { race。Enable() }}

2。5 關鍵核心機制

2。5。1 寫鎖對讀鎖的搶佔

加寫鎖的搶佔

// 在加寫鎖的時候透過將readerCount遞減最大允許加讀鎖的數量,來實現對加讀鎖的搶佔 r := atomic。AddInt32(&rw。readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders

加讀鎖的搶佔檢測

// 如果沒有寫鎖的情況下讀鎖的readerCount進行Add後一定是一個>0的數字,這裡透過檢測值為負數//就實現了讀鎖對寫鎖搶佔的檢測if atomic。AddInt32(&rw。readerCount, 1) < 0 { // A writer is pending, wait for it。 runtime_SemacquireMutex(&rw。readerSem, false) }

寫鎖搶佔讀鎖後後續的讀鎖就會加鎖失敗,但是如果想加寫鎖成功還要繼續對已經加讀鎖成功的進行等待

if r != 0 && atomic。AddInt32(&rw。readerWait, r) != 0 { // 寫鎖發現需要等待的讀鎖釋放的數量不為0,就自己自己去休眠了 runtime_SemacquireMutex(&rw。writerSem, false) }

寫鎖既然休眠了,則必定要有一種喚醒機制其實就是每次釋放鎖的時候,當檢查到有加寫鎖的情況下,就遞減readerWait,並由最後一個釋放reader lock的goroutine來實現喚醒寫鎖

if atomic。AddInt32(&rw。readerWait, -1) == 0 { // The last reader unblocks the writer。 runtime_Semrelease(&rw。writerSem, false) }

2。5。2 寫鎖的公平性

在加寫鎖的時候必須先進行mutex的加鎖,而mutex本身在普通模式下是非公平的,只有在飢餓模式下才是公平的

rw。w。Lock()

2。5。3 寫鎖與讀鎖的公平性

在加讀鎖和寫鎖的工程中都使用atomic。AddInt32來進行遞增,而該指令在底層是會透過LOCK來進行CPU匯流排加鎖的,因此多個CPU同時執行readerCount其實只會有一個成功,從這上面看其實是寫鎖與讀鎖之間是相對公平的,誰先達到誰先被CPU排程執行,進行LOCK鎖cache line成功,誰就加成功鎖

2。5。4 可見性與原子性問題

在併發場景中特別是JAVA中通常會提到併發裡面的兩個問題:可見性與記憶體屏障、原子性, 其中可見性通常是指在cpu多級快取下如何保證快取的一致性,即在一個CPU上修改了了某個資料在其他的CPU上不會繼續讀取舊的資料,記憶體屏障通常是為了CPU為了提高流水線效能,而對指令進行重排序而來,而原子性則是指的執行某個操作的過程的不可分割

go裡面並沒有volatile這種關鍵字,那如何能保證上面的AddInt32這個操作可以滿足上面的兩個問題呢, 其實關鍵就在於底層的2條指令,透過LOCK指令配合CPU的MESI協議,實現可見性和記憶體屏障,同時透過XADDL則用來保證原子性,從而解決上面提到的可見性與原子性問題

// atomic/asm_amd64。s TEXT runtime∕internal∕atomic·Xadd(SB) LOCK XADDL AX, 0(BX)