go語言之RWMutex

RWMutex特點

讀寫鎖區別與互斥鎖的主要區別就是讀鎖之間是共享的,多個goroutine可以同時加讀鎖,但是寫鎖與寫鎖、寫鎖與讀鎖之間則是互斥的。
因爲讀鎖是共享的,所以如果當前已經有讀鎖,那後續goroutine繼續加讀鎖正常情況下是可以加鎖成功,但是如果一直有讀鎖進行加鎖,那嘗試加寫鎖的goroutine則可能會長期獲取不到鎖,這就是因爲讀鎖而導致的寫鎖飢餓問題。如何解決寫鎖飢餓問題?

RWMutex的數據結構

type RWMutex struct {
    w           Mutex  // 互斥鎖
    writerSem   uint32 // 用於writer等待讀完成排隊的信號量
    readerSem   uint32 // 用於reader等待寫完成排隊的信號量
    readerCount int32  // 讀鎖的計數器
    readerWait  int32  // 等待讀鎖釋放的數量
}

const rwmutexMaxReaders = 1 << 30   // 支持最多2^30個讀鎖

在go裏面對寫鎖的計數採用了負值進行,通過遞減最大允許加讀鎖的數量從而進行寫鎖對讀鎖的搶佔。

讀鎖實現

RLock加讀鎖實現

func (rw *RWMutex) RLock() {
	if race.Enabled {
		_ = rw.w.state
		race.Disable()
	}
	// 累加reader計數器,累加結果如果小於0則表明有writer正在等待
	// 每次 goroutine獲取讀鎖時:readCount+1
    // 如果寫鎖已經被獲取,那麼 readCount 在 -rwmutexMaxReaders 與 0 之間(當爲0的時候,代表有2^30個讀鎖在等待,應該會出錯,但是極端條件不會出現)
   // 通過readCount 判斷讀鎖與寫鎖是否互斥,如果有寫鎖存在就掛起 goroutine,多個讀鎖可以並行
	if atomic.AddInt32(&rw.readerCount, 1) < 0 {
		 // 當前有writer正在等待讀鎖,等待讀信號量喚醒,喚醒的時機:寫鎖釋放的時候
		runtime_SemacquireMutex(&rw.readerSem, false, 0)
	}
	if race.Enabled {
		race.Enable()
		race.Acquire(unsafe.Pointer(&rw.readerSem))
	}
}

RUnlock釋放讀鎖實現

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 {
		// 1.r+1==0時,rw.readerCount -1= -1,rw.readerCount  = 0則不存在讀鎖,表示直接執行RUnlock()
       // 2.r+1=-rwmutexMaxReaders,rw.readerCount = -rwmutexMaxReaders ,
       // 這種情況出現在獲取Lock()方法,atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders),這時rw.readerCount  = 0 也不存在讀鎖,表示執行Lock()再執行RUnlock()
		if r+1 == 0 || r+1 == -rwmutexMaxReaders { //如果已經沒有讀鎖的,還去釋放(如釋放多次)
			race.Enable()
			throw("sync: RUnlock of unlocked RWMutex")
		}
		// 全部讀鎖釋放完畢後,釋放寫信號量
		if atomic.AddInt32(&rw.readerWait, -1) == 0 {
			// 釋放寫信號量
			runtime_Semrelease(&rw.writerSem, false, 1) 
		}
	}
	if race.Enabled {
		race.Enable()
	}
}

Lock寫鎖加鎖實現

func (rw *RWMutex) Lock() {
	if race.Enabled {
		_ = rw.w.state
		race.Disable()
	}
	// 保證寫鎖唯一
	rw.w.Lock()
	//  對readerCounter進行進行搶佔,通過遞減rwmutexMaxReaders允許最大讀的數量
	// 來實現寫鎖對讀鎖的搶佔
	r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders
	// 若有讀鎖,則等待獲取寫信號來。寫信號量什麼時候釋放:全部讀鎖釋放完畢
	if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {
		runtime_SemacquireMutex(&rw.writerSem, false, 0)
	}
	if race.Enabled {
		race.Enable()
		race.Acquire(unsafe.Pointer(&rw.readerSem))
		race.Acquire(unsafe.Pointer(&rw.writerSem))
	}
}

Unlock寫鎖釋放實現

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, 0)
	}
	// 釋放mutex
	rw.w.Unlock()
	if race.Enabled {
		race.Enable()
	}
}

關鍵核心機制

寫鎖對讀鎖的搶佔

1. 加寫鎖的搶佔
在加寫鎖的的時候,更改rw.readerCount的值(加寫鎖的搶佔)
r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders
2. 加讀鎖的時候檢測是否加了寫鎖
同時在加讀鎖的時候,判斷rw.readerCount的值(讀鎖對寫鎖的搶佔檢測)
if atomic.AddInt32(&rw.readerCount, 1) < 0 {
	 // 當前有writer正在等待讀鎖,等待讀信號量喚醒,喚醒的時機:寫鎖釋放的時候
	runtime_SemacquireMutex(&rw.readerSem, false, 0)
}
3. 加寫鎖的時候,發現前面還有寫鎖,則等待前面的讀鎖全部釋放完畢
if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {
    // 寫鎖發現需要等待的讀鎖釋放的數量不爲0,就自己自己去休眠了
    runtime_SemacquireMutex(&rw.writerSem, false)
}
4. 寫鎖休眠。必定什麼時候可以進行喚醒寫鎖——等待前面的所有讀鎖都釋放,即讀鎖釋放的代碼實現
if atomic.AddInt32(&rw.readerWait, -1) == 0 {
    // The last reader unblocks the writer.
    runtime_Semrelease(&rw.writerSem, false)
}

寫鎖的公平性

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

讀鎖與寫鎖的公平性

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

可見性與原子性

在併發場景中特別是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)

參考文獻

圖解golang裏面的讀寫鎖實現與核心原理分析瞭解編程語言背後設計
sync包 rwmutex源碼閱讀

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章