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

互斥鎖(百科)定義:“在編程中,引入了對象互斥鎖的概念,來保證共享數據操作的完整性。每個對象都對應於一個可稱爲" 互斥鎖" 的標記,這個標記用來保證在任一時刻,只能有一個線程訪問該對象。”,顧名思義就是互相排斥的鎖了。

當程序中就一個協程時,不需要加鎖,但是實際工程中不會只有單協程,可能有很多協程同時訪問公共資源,所以這個時候就需要用到鎖,那麼使用鎖的場景主要有哪些呢?

  1. 多個協程同時讀相同的數據時
  2. 多個協程同時寫相同的數據時
  3. 同一個資源,同時有讀和寫操作時

讀寫鎖之後,我們繼續來說說互斥鎖,互斥鎖從原理上來說要比讀寫鎖複雜一些,在Go語言中提供了sync.Mutex標準庫,Mutex結構體來定義。Mutex同樣繼承於Locker接口。

互斥鎖特點:一次只能一個協程擁有互斥鎖,其他線程只有等待。

源碼基於:go version go1.13.4 windows/amd64。

兩種操作模式:

  1. 正常模式:所有協程以先進先出(FIFO)方式進行排隊,被喚醒的協程同樣需要競爭方式爭奪鎖,新協程爭搶會有優勢,因爲他們已經運行在CPU上,更容易搶到鎖,如果一個協程在等待超過1毫秒會自動切換到飢餓模式下。
  2. 飢餓模式:互斥鎖會直接由解鎖的協程交給隊列頭部的等待者,新爭搶者不能直接獲得鎖,不嘗試自旋,會老老實實的等。

兩種工作模式:

  1. 競爭模式:所有協程一起搶
  2. 隊列模式:所有協程一起排隊

這兩種工作模式會通過一些情況進行切換的。

互斥鎖的定義

type Mutex struct {
	state int32  // 互斥鎖上鎖狀態
	sema  uint32 // 信號量
}

state=0時是未上鎖,state=1時是鎖定狀態。

互斥鎖常量的定義

const (
	mutexLocked           = 1 << iota // 1
	mutexWoken                        // 2
	mutexStarving                     // 4
	mutexWaiterShift      = iota      // 3
	starvationThresholdNs = 1e6       // 1e+06
)

看一下互斥鎖的結構主要方法,主要有Lock()和Unlonk()方法組成,使用Lock()加鎖後便不能再次對其加鎖操作,直到Unlock()解鎖後才能再次加鎖,適用於讀寫不確定的場景,並且只允許只有一個讀或者寫的場景。

Lock()

func (m *Mutex) Lock() {
	// ①CAS嘗試獲取鎖,state爲0表示沒有協程持有鎖,直接獲得鎖,將mutexLocked置爲1。
	// 如果設置成功,直接返回。如果獲取鎖失敗會進入lockSlow方法進行自旋搶鎖,直到搶到鎖後返回。
	if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
		if race.Enabled {
			race.Acquire(unsafe.Pointer(m))
		}
		return
	}
	m.lockSlow()
}

①CAS嘗試獲取鎖,state爲0表示沒有協程持有鎖,直接獲得鎖,將mutexLocked置爲1。如果設置成功,直接返回。如果獲取鎖失敗會進入lockSlow方法進行自旋搶鎖,直到搶到鎖後返回。

lockSlow()

func (m *Mutex) lockSlow() {
	var waitStartTime int64
	starving := false
	awoke := false // 循環標記
	iter := 0      // 計數器
	old := m.state // 當前的鎖狀態
	for {
		// ①old與(1或4),如果等於1,說明已經加過鎖,協程是否開始自旋
		if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
			// ②當前協程未更新成功mutexWoken位,mutexWoken位仍然爲0,等待隊列非空
			// ②更新mutexWoken位成功,且可以自旋
			if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
				atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
				awoke = true
			}
			// 主動自旋,暫停一段時間
			runtime_doSpin()
			iter++
			old = m.state
			continue
		}
		new := old
		// ③第三位如果等於0,爲正常模式,new=1表示可以拿到鎖
		if old&mutexStarving == 0 {
			new |= mutexLocked
		}
		// ④當old的1和3位爲1時,爲飢餓模式,需要去排隊
		if old&(mutexLocked|mutexStarving) != 0 {
			new += 1 << mutexWaiterShift
		}
		// ⑤已經標記飢餓模式,還未鎖住,new設置爲飯餓模式
		if starving && old&mutexLocked != 0 {
			new |= mutexStarving
		}
		// ⑥喚醒
		if awoke {
			// ⑦互斥狀態不相同就panic
			if new&mutexWoken == 0 {
				throw("sync: inconsistent mutex state")
			}
			// 把awoke位清掉
			new &^= mutexWoken
		}
		if atomic.CompareAndSwapInt32(&m.state, old, new) {
			// ⑧獲取鎖成功就返回
			if old&(mutexLocked|mutexStarving) == 0 {
				break // locked the mutex with CAS
			}
			// ⑨被喚醒的協程搶鎖失敗,重新放到隊列首部
			queueLifo := waitStartTime != 0
			if waitStartTime == 0 {
				waitStartTime = runtime_nanotime()
			}
			// ⑩進入休眠狀態,等待信號喚醒
			runtime_SemacquireMutex(&m.sema, queueLifo, 1)
			starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
			old = m.state
			if old&mutexStarving != 0 {
				// ⑾飢餓模式不會出現mutex被鎖住|喚醒,等待隊列不能爲0
				if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {
					throw("sync: inconsistent mutex state")
				}
				// ⑿拿到鎖,等待數-1
				delta := int32(mutexLocked - 1<<mutexWaiterShift)
				if !starving || old>>mutexWaiterShift == 1 {
					delta -= mutexStarving
				}
				// ⒀更新狀態,高位原子計數,直接添加
				atomic.AddInt32(&m.state, delta)
				break
			}
			awoke = true
			iter = 0
		} else {
			old = m.state
		}
	}

	if race.Enabled {
		race.Acquire(unsafe.Pointer(m))
	}
}

lockSlow()方法進入時進入循環體,先將①old與(1或4),如果等於1,說明已經加過鎖,協程是否開始自旋。②當前協程未更新成功mutexWoken位,mutexWoken位仍然爲0,等待隊列非空,自旋。③第三位如果等於0,爲正常模式,new=1表示可以拿到鎖。④當old的1和3位爲1時,爲飢餓模式,需要去排隊。⑤已經標記飢餓模式,還未鎖住,new設置爲飯餓模式。如果喚醒,喚醒時⑦互斥狀態不相同就panic,否則把awoke位清掉。⑧獲取鎖成功就返回,這時纔拿到互斥鎖。⑨被喚醒的協程搶鎖失敗,重新放到隊列首部。⑩進入休眠狀態,等待信號喚醒,⑾飢餓模式不會出現mutex被鎖住|喚醒,等待隊列不能爲0。⑿拿到鎖,等待數-1。⒀更新狀態,高位原子計數,直接添加。

Unlock()

func (m *Mutex) Unlock() {
	if race.Enabled {
		_ = m.state
		race.Release(unsafe.Pointer(m))
	}

	// 直接解鎖
	new := atomic.AddInt32(&m.state, -mutexLocked)
	if new != 0 {

		m.unlockSlow(new)
	}
}

Unlock直接state-mutexLocked解鎖,如果減失敗,進入unlockSlow方法進行解鎖。

unlockSlow()

func (m *Mutex) unlockSlow(new int32) {
	// ①狀態不一致,直接拋異常
	if (new+mutexLocked)&mutexLocked == 0 {
		throw("sync: unlock of unlocked mutex")
	}
	// ②飢餓模式直接喚醒隊列首部的協程
	if new&mutexStarving == 0 {
		old := new
		for {
			// ③如果沒有等待者或協程,不用喚醒就返回
			if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
				return
			}

			new = (old - 1<<mutexWaiterShift) | mutexWoken
			if atomic.CompareAndSwapInt32(&m.state, old, new) {
				runtime_Semrelease(&m.sema, false, 1)
				return
			}
			old = m.state
		}
	} else {
		// ④飢餓模式下,將持有鎖交給下一個等待者,此時mutexLocked還爲0,但是在飢餓模式下,新協程不會更新mutexLocked位。
		runtime_Semrelease(&m.sema, true, 1)
	}
}

unlockSlow()方法進入時,如果對未上鎖的進行解鎖則拋異常①狀態不一致,直接拋異常。②飢餓模式直接喚醒隊列首部的協程。循環檢測③如果沒有等待者或協程,不用喚醒就返回。否則的話④飢餓模式下,將持有鎖交給下一個等待者,此時mutexLocked還爲0,但是在飢餓模式下,新協程不會更新mutexLocked位。

總結

互斥鎖只能鎖定一次,當在解鎖之前再次進行加鎖,便會無法加鎖。如果在加鎖前解鎖,便會報錯"panic: sync: unlock of unlocked mutex"。 

互斥鎖無衝突,有衝突時,首先自旋,經過短暫自旋後可以獲得鎖,如果自旋無結果時通過信號通知協程繼續等待。

 

 

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