互斥鎖Mutex
type Mutex struct {// 表示互斥鎖狀態state int32// 表示信號量,協程阻塞等待該信號量,解鎖的協程釋放信號量從而喚醒等待信號量的協程sema uint32
}
Locked: 表示該Mutex是否已被鎖定,0:沒有鎖定 1:已被鎖定
Woken: 表示是否有協程已被喚醒,0:沒有協程喚醒 1:已有協程喚醒,正在加鎖過程中
Starving:表示該Mutex是否處于饑餓狀態,0:沒有饑餓 1:饑餓狀態,說明有協程阻塞了超過1ms
Waiter: 表示阻塞等待鎖的協程個數,協程解鎖時根據此值來判斷是否需要釋放信號量
簡單加鎖
判斷Locked標志位是否為0,如果是0則把Locked置1,代表加鎖成功
加鎖被阻塞
加鎖前,如果Locked為1,則將Waiter+1,表示此協程阻塞等待,直到Locked為0后被喚醒
簡單解鎖
如果沒有其他協程阻塞等待加鎖,Waiter為0,則直接把Locked置0,
解鎖并喚醒協程
如果Waiter大于0,有其他協程阻塞等待加鎖,則當前協程解鎖:將Locked置0;釋放一個信號量,喚醒一個阻塞協程
自旋過程
自旋相當于CPU空轉,sleep一小段時間。自旋過程當前協程會持續探測Locked是否改為0,如果改為0則可以直接運行,不必進入阻塞狀態。可以充分利用CPU,避免協程切換
問題:如果加鎖的協程特別多,每次都通過自旋獲得鎖,那么之前被阻塞的進程將很難獲得鎖,從而進入饑餓狀態。1.8版本以來增加了一個狀態,即Mutex的Starving狀態。這個狀態下不會自旋,一旦有協程釋放鎖,那么一定會喚醒一個協程并成功加鎖
模式
normal模式
默認模式,該模式下,協程如果加鎖不成功不會立即轉入阻塞排隊,而是判斷是否滿足自旋的條件,如果滿足則會啟動自旋過程,嘗試搶鎖
starvation模式
處于饑餓模式下,不會啟動自旋過程,也即一旦有協程釋放了鎖,那么一定會喚醒協程,被喚醒的協程將會成功獲取鎖,同時也會把等待計數減1
讀寫鎖RWMutex
讀讀不互斥,讀寫互斥,寫寫互斥
P操作 信號量值 -1,如果小于0就阻塞等待;V操作 信號量值 +1,喚醒阻塞的線程
讀者和寫者加鎖時,進行P操作;讀者和寫者釋放鎖時,進行V操作
寫者加鎖和讀者釋放鎖,操作writerSem;讀者加鎖和寫者釋放鎖,操作readerSem
type RWMutex struct {// 控制寫鎖,獲得寫鎖首先要獲取該鎖,實現了寫寫互斥w Mutex // 寫阻塞等待的信號量,最后一個讀者釋放鎖時會釋放此信號量,通知寫者進行寫操作writerSem uint32 // 讀阻塞等待的信號量,持有寫鎖的協程釋放鎖后會釋放此信號量,通知讀者進行讀操作readerSem uint32 // 實現了讀寫互斥,讀讀不互斥// 只要有讀操作到來,則此字段+1(讀讀不互斥)// 1.記錄當前持有讀鎖(正在讀或者等待寫完再讀)的協程數量 2.<0代表有寫者在等(讀寫互斥)readerCount int32 // 記錄寫阻塞時需要等待多少個讀者釋放讀鎖(實現寫操作不會被餓死)readerWait int32
}
源碼
寫者加鎖Lock()
const rwmutexMaxReaders = 1 << 30func (rw *RWMutex) Lock() {// 先獲取互斥鎖rw.w.Lock()// 先將readerCount加一個很大的負數使其<0(表示當前有寫者正在等或執行),然后用r記錄原來的readerCountr := rw.readerCount.Add(-rwmutexMaxReaders) + rwmutexMaxReaders// r!=0說明當前還有讀者正在讀,隨后讓readerWait加上r(readerCount),表示要等待的讀者完成的個數if r != 0 && rw.readerWait.Add(r) != 0 {// runtime_SemacquireRWMutex()阻塞當前goroutine,直到某個條件滿足釋放信號量的操作// 當前寫者需要阻塞(writerSem信號量為0則阻塞),直到所有讀者釋放鎖(信號量大于0則執行)runtime_SemacquireRWMutex(&rw.writerSem, false, 0)}
}
先獲取寫鎖
將readerCount加一個很大的負數使其<0,表示當前有寫者正在等待或執行
將readerWait加上原本的readerCount,表示當前寫操作之前要等待的讀操作數量
如果readerCount和readerWait都不為0,則等最后一個讀操作執行完畢釋放writerSem信號量,再執行寫操作
讀者加鎖RLock()
func (rw *RWMutex) RLock() {// 讀者數量+1(先加上讀鎖),然后判斷如果小于0表示有寫者持有寫鎖,則不能讀者立即加鎖if rw.readerCount.Add(1) < 0 {// 讀者等待寫者信號量釋放runtime_SemacquireRWMutexR(&rw.readerSem, false, 0)}
}
先readerCount+1,將讀操作入隊排隊
等寫操作執行完(釋放readerSem信號量),再執行讀操作
寫者釋放鎖UnLock()
func (rw *RWMutex) Unlock() {// 在Lock加鎖的時候對readerCount減去了rwmutexMaxReaders,這時加回來還原恢復r := rw.readerCount.Add(rwmutexMaxReaders)// 加rwmutexMaxReaders之前,readerCount減去了rwmutexMaxReaders// 如果這個readerCount>=0,說明沒有寫者if r >= rwmutexMaxReaders {race.Enable()fatal("sync: Unlock of unlocked RWMutex")}// 當前寫鎖期間累積了多少個阻塞的讀者(readerCount),就釋放幾次readerSemfor i := 0; i < int(r); i++ {runtime_Semrelease(&rw.readerSem, false, 0)}// 釋放寫鎖rw.w.Unlock()
}
恢復readerCount為原本的值
有多少個readerCount(寫操作期間,后來的讀操作被阻塞等待,讀操作數量),就釋放幾次readerSem信號量,將所有等待的讀操作全部喚醒
釋放寫鎖
讀者釋放鎖RUnLock()
func (rw *RWMutex) RUnlock() {// 先將readerCount減1,釋放讀鎖。如果readerCount<0,表示有寫者在等,則進入rUnlockSlow()if r := rw.readerCount.Add(-1); r < 0 {rw.rUnlockSlow(r)}
}func (rw *RWMutex) rUnlockSlow(r int32) {// 邊界問題處理// r+1 ==0 表示沒有讀者加鎖,卻調用了釋放讀鎖// r+1 == -rwmutexMaxReaders表示在沒有讀者加鎖,有寫者持有互斥鎖的情況下,卻釋放了讀鎖if r+1 == 0 || r+1 == -rwmutexMaxReaders {race.Enable()fatal("sync: RUnlock of unlocked RWMutex")}// readerWait-1,將寫者需要等待的讀者-1// 如果readerWait-1后為0,表示這是最后一個讀者,要發送信號量通知寫者if rw.readerWait.Add(-1) == 0 {runtime_Semrelease(&rw.writerSem, false, 1)}
}
先釋放讀鎖(readerCount-1)
如果有寫者在等(readerCount<0),并且當此讀者為寫操作需要等待的最后一個讀者時(readerWait-1==0),釋放writerSem信號量通知寫者進行寫操作
寫操作如何阻止讀操作(正在寫操作,則不能讀)
每次讀鎖定將readerCount值+1,每次解除讀鎖定將該值-1
寫鎖定進行時,會先將readerCount減去固定的大數,從而readerCount變成負值,此時再有讀鎖定到來時檢測到readerCount為負值,便知道有寫操作在進行,只好阻塞等待。而真實的讀操作個數并不會丟失,只需要將readerCount加上固定大數即可獲得
所以,寫操作將readerCount變成負值來阻止讀操作的
讀操作如何阻止寫操作(正在讀操作,則不能寫)
讀鎖定會先將readerCount加1,此時寫操作到來時發現讀者數量readerCount不為0,會阻塞等待所有讀操作結束
所以,讀操作通過readerCount來將來阻止寫操作的
為什么寫鎖定不會被餓死(寫優先)
寫操作要等待讀操作結束后才可以獲得鎖,寫操作等待期間可能還有新的讀操作持續到來,如果寫操作等待所有讀操作結束,很可能被餓死
readerWait標記寫操作到來時,前面正在執行的讀操作的個數,等這些讀操作完畢后readerWait減為0,通知寫操作執行(由于只要有讀操作到來,都會將readerCount+1,所以無法知道讀操作的先后順序,這里用readerWait臨時記錄寫操作到來時前面的讀操作個數,實現了給寫操作“排隊”的效果,使寫操作不會被后續讀操作“插隊”)
寫操作到來時,會把readerCount值拷貝到readerWait中,用于標記排在寫操作前面的讀者個數
寫操作前面的讀操作結束后,除了會遞減readerCount,還會遞減RWMutex.readerWait,當readerWait值變為0時(寫操作前面的讀操作都已結束),喚醒寫操作
? ? ? 2. 同時寫操作一旦到來,就會將readerCount改為負值,表示有寫操作在等待,使得寫操作不會被后續到來的讀操作搶占