我們在多協程操作時,有種場景是讀操作次數遠遠大於寫操作,這個時候,我們就會考慮用到讀寫鎖。
讀寫鎖
讀寫鎖(百科)定義:是一種特殊的的自旋鎖,它把對共享資源的訪問者劃分成讀者和寫者,讀者只對共享資源進行讀訪問,寫者則需要對共享資源進行寫操作。
在Go語言中、sync標準庫定義了RWMutex結構代表讀寫鎖,該結構在rwmutex.go中,RWMutex繼承於Locker接口,其實是互斥鎖的改進版,爲什麼這樣說呢,看完下文就會清楚了。
讀寫鎖特徵:
讀鎖之間是共享的,多協程可以同時加讀鎖,寫鎖與讀鎖之間是互斥的。
源碼基於:go version go1.13.4 windows/amd64。
Locker接口
type Locker interface {
Lock()
Unlock()
}
讀寫鎖結構體定義
type RWMutex struct {
w Mutex // 互斥鎖,寫鎖協程獲取該鎖後,其他寫鎖處於阻塞等待
writerSem uint32 // 寫入等待信號量,最後一個讀取協程釋放鎖時會釋放信號量
readerSem uint32 // 讀取等待信號量,持有寫鎖協程釋放後會釋放信號量
readerCount int32 // 讀鎖的個數
readerWait int32 // 寫操作時,需要等待讀操作的個數
}
我們看到RWMutex讀寫鎖裏面包含:
- w Mutex互斥鎖,w控制併發時多個寫操作
- writerSem 寫入等待信號量
- readerSem 讀取等待信號量
- readerCount 讀鎖的個數
- readerWait 寫操作時,需要等待讀操作的個數
再看常量rwmutexMaxReaders,定義的是最大讀鎖數量
// rwmutexMaxReaders:支持1073741824個鎖
const rwmutexMaxReaders = 1 << 30
sync.RWMutex所有方法如下圖所示
讀寫鎖的核心方法:其主要核心的有Lock、Unlock、RLock和RUnlock四個方法。
func (*RWMutex) Lock // 獲取寫鎖操作
func (*RWMutex) Unlock // 解除寫鎖操作
func (*RWMutex) RLock // 獲取讀鎖操作
func (*RWMutex) RUnlock // 解除讀鎖操作
RLocker() Locker // 返回Locker對象
RLocker,返回一個Locker對象
// 返回一個Locker對象
func (rw *RWMutex) RLocker() Locker {
return (*rlocker)(rw)
}
Lock,獲取寫鎖操作
func (rw *RWMutex) Lock() {
// ①、競爭鎖時檢測
if race.Enabled {
_ = rw.w.state
race.Disable()
}
// 先獲取Mutex互斥鎖,多協程時只能有一個協程拿到鎖
rw.w.Lock()
// ②、寫鎖時,會將readerCount減去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))
}
}
①寫鎖操作只能被一個寫協程拿到,競爭檢測後進行互斥鎖上鎖Mutex.lock,,多協程時只能有一個協程拿到鎖
②寫鎖時,readerCount減去rwmutexMaxReaders,readerCount會變成很大的負數,讀鎖時readerCount會+1
③如果readerCount不等於0,協程獲取到讀鎖,如果滿足上鎖條件時,會調用runtime_SemacquireMutex,如果先進來的會在隊列前面阻塞等待的信號量爲writerSem,進入隊列等待。
Unlock,解除寫鎖操作
func (rw *RWMutex) Unlock() {
// ①競爭檢測
if race.Enabled {
_ = rw.w.state
race.Release(unsafe.Pointer(&rw.readerSem))
race.Disable()
}
// ②需要回復readerCount,上鎖的時候減了常量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)
}
// ⑤釋放鎖,如果有其他阻塞寫操作,在此喚醒
rw.w.Unlock()
if race.Enabled {
race.Enable()
}
}
①解除寫鎖操作時,競爭檢測
②需要回復readerCount,上鎖的時候減了常量rwmutexMaxReaders,這裏再加回來,加完之後如果大於等於rwmutexMaxReaders
③對未被寫鎖定的讀寫鎖進行寫解鎖,會拋異常
④呼喚阻塞的讀操作
⑤釋放鎖,如果有其他阻塞寫操作,在此喚醒
RLock,獲取讀鎖操作
func (rw *RWMutex) RLock() {
if race.Enabled {
_ = rw.w.state
race.Disable()
}
// ①讀鎖時readerCount+1操作,當值小於0時會調用runtime_SemacquireMutex,表明寫鎖操作正在等待
if atomic.AddInt32(&rw.readerCount, 1) < 0 {
// 當寫鎖操作時,讀鎖也會阻塞等待
runtime_SemacquireMutex(&rw.readerSem, false, 0)
}
if race.Enabled {
race.Enable()
race.Acquire(unsafe.Pointer(&rw.readerSem))
}
}
①①讀鎖時readerCount+1操作,當值小於0時會調用runtime_SemacquireMutex,表明寫鎖操作正在等待
②當寫鎖操作時,讀鎖也會阻塞等待,讀鎖加入等待隊列
RUnlock,解除讀鎖操作
func (rw *RWMutex) RUnlock() {
if race.Enabled {
_ = rw.w.state
race.ReleaseMerge(unsafe.Pointer(&rw.writerSem))
race.Disable()
}
// ①直接將readerCount-1操作
if r := atomic.AddInt32(&rw.readerCount, -1); r < 0 {
// ②如果r小於0,說明有協程正在獲取寫操作鎖,
// 當readerWait減到0時說明沒有協程持有寫操作鎖,
// 就通過信息號wrirerSem通知等待的協程來爭搶鎖
rw.rUnlockSlow(r)
}
if race.Enabled {
race.Enable()
}
}
①解除讀鎖,直接對readerCount-1操作,這裏就是把之前+1操作的再減回來
②如果r小於0,說明有協程正在獲取寫操作鎖,當readerWait減到0時說明沒有協程持有寫操作鎖,就通過信息號wrirerSem通知等待的協程來爭搶鎖
總結
- 寫鎖操作時,對讀寫鎖進行讀鎖定和寫鎖定,都會阻塞,讀鎖與寫鎖之間是互斥的。
- 讀鎖操作時,對讀寫鎖進行寫鎖定,會阻塞,加讀鎖時不會阻塞。
- 對未被上鎖的讀|寫鎖進行解除鎖定操作,會Panic。
- 解除寫鎖操作時,同時會喚醒所有阻塞的讀鎖協程。
- 解除讀鎖操作時,會喚醒一個因寫鎖操作而被阻塞的協程。
- 讀鎖存在的時候,同時出現讀鎖和寫鎖操作,優先獲取寫鎖。
- 同時2個協程去爭搶讀鎖和寫鎖時,都有機會搶成功。
- 寫鎖存在時,同時開啓2個協程去爭搶讀鎖和寫鎖,優先獲得讀鎖。
本文爲原創文章,出自guichenglin,轉載請粘貼源鏈接,如果未經允許轉發後果自負。