互斥鎖(百科)定義:“在編程中,引入了對象互斥鎖的概念,來保證共享數據操作的完整性。每個對象都對應於一個可稱爲" 互斥鎖" 的標記,這個標記用來保證在任一時刻,只能有一個線程訪問該對象。”,顧名思義就是互相排斥的鎖了。
當程序中就一個協程時,不需要加鎖,但是實際工程中不會只有單協程,可能有很多協程同時訪問公共資源,所以這個時候就需要用到鎖,那麼使用鎖的場景主要有哪些呢?
- 多個協程同時讀相同的數據時
- 多個協程同時寫相同的數據時
- 同一個資源,同時有讀和寫操作時
讀寫鎖之後,我們繼續來說說互斥鎖,互斥鎖從原理上來說要比讀寫鎖複雜一些,在Go語言中提供了sync.Mutex標準庫,Mutex結構體來定義。Mutex同樣繼承於Locker接口。
互斥鎖特點:一次只能一個協程擁有互斥鎖,其他線程只有等待。
源碼基於:go version go1.13.4 windows/amd64。
兩種操作模式:
- 正常模式:所有協程以先進先出(FIFO)方式進行排隊,被喚醒的協程同樣需要競爭方式爭奪鎖,新協程爭搶會有優勢,因爲他們已經運行在CPU上,更容易搶到鎖,如果一個協程在等待超過1毫秒會自動切換到飢餓模式下。
- 飢餓模式:互斥鎖會直接由解鎖的協程交給隊列頭部的等待者,新爭搶者不能直接獲得鎖,不嘗試自旋,會老老實實的等。
兩種工作模式:
- 競爭模式:所有協程一起搶
- 隊列模式:所有協程一起排隊
這兩種工作模式會通過一些情況進行切換的。
互斥鎖的定義
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"。
互斥鎖無衝突,有衝突時,首先自旋,經過短暫自旋後可以獲得鎖,如果自旋無結果時通過信號通知協程繼續等待。