Go語言 讀寫鎖&互斥鎖原理剖析(1)

我們在多協程操作時,有種場景是讀操作次數遠遠大於寫操作,這個時候,我們就會考慮用到讀寫鎖。

讀寫鎖

讀寫鎖(百科)定義:是一種特殊的的自旋鎖,它把對共享資源的訪問者劃分成讀者和寫者,讀者只對共享資源進行讀訪問,寫者則需要對共享資源進行寫操作。

在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讀寫鎖裏面包含:

  1. w Mutex互斥鎖,w控制併發時多個寫操作
  2. writerSem 寫入等待信號量
  3. readerSem 讀取等待信號量
  4. readerCount 讀鎖的個數
  5. 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,轉載請粘貼源鏈接,如果未經允許轉發後果自負。

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