一绘沉、鎖的基礎(chǔ)知識(shí)
1. 互斥量/互斥鎖
互斥量(Mutex), 又稱(chēng)為互斥鎖砂吞, 是一種用來(lái)保護(hù)臨界區(qū)的特殊變量铛铁, 它可以處于鎖定(locked) 狀態(tài)隔显, 也可以處于解鎖(unlocked) 狀態(tài)。
在編程中饵逐,引入了對(duì)象互斥鎖的概念括眠,來(lái)保證共享數(shù)據(jù)操作的完整性。每個(gè)對(duì)象都對(duì)應(yīng)于一個(gè)可稱(chēng)為" 互斥鎖" 的標(biāo)記倍权,這個(gè)標(biāo)記用來(lái)保證在任一時(shí)刻掷豺,只能有一個(gè)線程訪問(wèn)該對(duì)象。
2. CAS(compare and swap)
解決多線程并行情況下使用鎖造成性能損耗的一種機(jī)制薄声,CAS操作包含三個(gè)操作數(shù)——內(nèi)存位置(V)当船、預(yù)期原值(A)和新值(B)。如果內(nèi)存位置的值與預(yù)期原值相匹配默辨,那么處理器會(huì)自動(dòng)將該位置值更新為新值德频。否則,處理器不做任何操作缩幸。無(wú)論哪種情況壹置,它都會(huì)在CAS指令之前返回該位置的值。CAS有效地說(shuō)明了“我認(rèn)為位置V應(yīng)該包含值A(chǔ)表谊;如果包含該值蒸绩,則將B放到這個(gè)位置;否則铃肯,不要更改該位置,只告訴我這個(gè)位置現(xiàn)在的值即可传蹈。"
CAS機(jī)制執(zhí)行流程:CAS存在的問(wèn)題:
1. ABA問(wèn)題
CAS需要在操作值的時(shí)候押逼,檢查值有沒(méi)有發(fā)生變化步藕,如果沒(méi)有發(fā)生變化就更新,但是如果一個(gè)值原來(lái)是A挑格,變成了B咙冗,又變成了A,那么使用CAS進(jìn)行檢查時(shí)會(huì)發(fā)現(xiàn)它的值沒(méi)有發(fā)生變化漂彤,但是實(shí)際上卻變化了 —> 這就是所謂的ABA問(wèn)題雾消。
ABA問(wèn)題的解決思路其實(shí)也很簡(jiǎn)單,就是使用版本號(hào)挫望。在變量前面追加上版本號(hào)立润,每次變量更新的時(shí)候把版本號(hào)加1,那么A→B→A就會(huì)變成1A→2B→3A了媳板。
2. 循環(huán)時(shí)間長(zhǎng)開(kāi)銷(xiāo)大
自旋CAS如果長(zhǎng)時(shí)間不成功桑腮,會(huì)給CPU帶來(lái)非常大的執(zhí)行開(kāi)銷(xiāo)
3. 只能保證一個(gè)共享變量的原子操作
當(dāng)對(duì)一個(gè)共享變量執(zhí)行操作時(shí),我們可以使用循環(huán)CAS的方式來(lái)保證原子操作蛉幸,但是對(duì)多個(gè)共享變量操作時(shí)破讨,循環(huán)CAS就無(wú)法保證操作的原子性,這個(gè)時(shí)候就可以用鎖奕纫。
3. 自旋鎖
自旋鎖與互斥鎖比較類(lèi)似提陶,它們都是為了解決對(duì)某項(xiàng)資源的互斥使用。無(wú)論是互斥鎖匹层,還是自旋鎖隙笆,在任何時(shí)刻,最多只能有一個(gè)保持者又固,也就說(shuō)仲器,在任何時(shí)刻最多只能有一個(gè)執(zhí)行單元獲得鎖。但是兩者在調(diào)度機(jī)制上略有不同仰冠。對(duì)于互斥鎖乏冀,如果資源已經(jīng)被占用,資源申請(qǐng)者只能進(jìn)入睡眠狀態(tài)洋只。但是自旋鎖不會(huì)引起調(diào)用者睡眠辆沦,如果自旋鎖已經(jīng)被別的執(zhí)行單元保持,調(diào)用者就一直循環(huán)在那里看是否該自旋鎖的保持者已經(jīng)釋放了鎖识虚,"自旋"一詞就是因此而得名肢扯。
自旋鎖可能存在的2個(gè)問(wèn)題:
試圖遞歸地獲得自旋鎖必然會(huì)引起死鎖:遞歸程序的持有實(shí)例在第二個(gè)實(shí)例循環(huán),以試圖獲得相同自旋鎖時(shí)担锤,不會(huì)釋放此自旋鎖蔚晨。
在遞歸程序中使用自旋鎖應(yīng)遵守下列策略:遞歸程序決不能在持有自旋鎖時(shí)調(diào)用它自己,也決不能在遞歸調(diào)用時(shí)試圖獲得相同的自旋鎖。過(guò)多占用cpu資源铭腕。如果不加限制银择,由于申請(qǐng)者一直在循環(huán)等待,因此自旋鎖在鎖定的時(shí)候,如果不成功,不會(huì)睡眠,會(huì)持續(xù)的嘗試,單cpu的時(shí)候自旋鎖會(huì)讓其它process動(dòng)不了. 因此累舷,一般自旋鎖實(shí)現(xiàn)會(huì)有一個(gè)參數(shù)限定最多持續(xù)嘗試次數(shù). 超出后, 自旋鎖放棄當(dāng)前time slice. 等下一次機(jī)會(huì)浩考。
由此可見(jiàn),自旋鎖比較適用于鎖使用者保持鎖時(shí)間比較短的情況被盈。正是由于自旋鎖使用者一般保持鎖時(shí)間非常短析孽,因此選擇自旋而不是睡眠是非常必要的,自旋鎖的效率遠(yuǎn)高于互斥鎖只怎。
4. 讀寫(xiě)鎖
讀寫(xiě)鎖實(shí)際是一種特殊的自旋鎖袜瞬,它把對(duì)共享資源的訪問(wèn)者劃分成讀者和寫(xiě)者,讀者只對(duì)共享資源進(jìn)行讀訪問(wèn)尝盼,寫(xiě)者則需要對(duì)共享資源進(jìn)行寫(xiě)操作吞滞。
這種鎖相對(duì)于自旋鎖而言,能提高并發(fā)性盾沫,因?yàn)樵诙嗵幚砥飨到y(tǒng)中裁赠,它允許同時(shí)有多個(gè)讀者來(lái)訪問(wèn)共享資源,最大可能的讀者數(shù)為實(shí)際的邏輯CPU數(shù)赴精。寫(xiě)者是排他性的佩捞,一個(gè)讀寫(xiě)鎖同時(shí)只能有一個(gè)寫(xiě)者或多個(gè)讀者(與CPU數(shù)相關(guān)),但不能同時(shí)既有讀者又有寫(xiě)者蕾哟。
在讀寫(xiě)鎖保持期間也是搶占失效的一忱。
如果讀寫(xiě)鎖當(dāng)前沒(méi)有讀者,也沒(méi)有寫(xiě)者谭确,那么寫(xiě)者可以立刻獲得讀寫(xiě)鎖帘营,否則它必須自旋在那里,直到?jīng)]有任何寫(xiě)者或讀者逐哈。如果讀寫(xiě)鎖沒(méi)有寫(xiě)者芬迄,那么讀者可以立即獲得該讀寫(xiě)鎖,否則讀者必須自旋在那里昂秃,直到寫(xiě)者釋放該讀寫(xiě)鎖禀梳。
5. 樂(lè)觀鎖 & 悲觀鎖
樂(lè)觀鎖其實(shí)主要就是一種思想,因?yàn)闃?lè)觀鎖的操作過(guò)程中其實(shí)沒(méi)有沒(méi)有任何鎖的參與肠骆,樂(lè)觀鎖只是和悲觀鎖相對(duì)算途,嚴(yán)格的說(shuō)樂(lè)觀鎖不能稱(chēng)之為鎖。
悲觀鎖:總是假設(shè)最壞的情況蚀腿,每次去拿數(shù)據(jù)的時(shí)候都認(rèn)為別人會(huì)修改嘴瓤,所以每次在拿數(shù)據(jù)的時(shí)候都會(huì)上鎖,這樣別人想拿這個(gè)數(shù)據(jù)就會(huì)阻塞,直到它拿到鎖(共享資源每次只給一個(gè)線程使用廓脆,其它線程阻塞畏浆,用完后再把資源轉(zhuǎn)讓給其它線程)。
樂(lè)觀鎖:總是假設(shè)最好的情況狞贱,每次去拿數(shù)據(jù)的時(shí)候都認(rèn)為別人不會(huì)修改,所以不會(huì)上鎖蜀涨,只在更新的時(shí)候會(huì)判斷一下在此期間別人有沒(méi)有去更新這個(gè)數(shù)據(jù)瞎嬉。
樂(lè)觀鎖適用于寫(xiě)比較少的情況下(多讀場(chǎng)景),即沖突真的很少發(fā)生的時(shí)候厚柳,這樣可以省去了鎖的開(kāi)銷(xiāo)氧枣,加大了系統(tǒng)的整個(gè)吞吐量。但如果是多寫(xiě)的情況别垮,一般會(huì)經(jīng)常產(chǎn)生沖突便监,這就會(huì)導(dǎo)致上層應(yīng)用會(huì)不斷的進(jìn)行retry,這樣反倒是降低了性能碳想,所以一般多寫(xiě)的場(chǎng)景下用悲觀鎖就比較合適烧董。
樂(lè)觀鎖常見(jiàn)的兩種實(shí)現(xiàn)方式:
1. 版本號(hào)機(jī)制
CAS機(jī)制保證了在更新數(shù)據(jù)的時(shí)候沒(méi)有被修改為其他數(shù)據(jù)的同步機(jī)制,版本機(jī)制就保證了沒(méi)有被修改過(guò)的同步機(jī)制
2. CAS機(jī)制
當(dāng)多個(gè)線程嘗試使用CAS同時(shí)更新同一個(gè)變量時(shí)胧奔,只有其中一個(gè)線程能更新變量的值逊移,而其它線程都失敗。
6. 死鎖
死鎖是指兩個(gè)或兩個(gè)以上的進(jìn)程在執(zhí)行過(guò)程中龙填,由于競(jìng)爭(zhēng)資源或者由于彼此通信而造成的一種阻塞的現(xiàn)象胳泉,若無(wú)外力作用,它們都將無(wú)法推進(jìn)下去岩遗。此時(shí)稱(chēng)系統(tǒng)處于死鎖狀態(tài)或系統(tǒng)產(chǎn)生了死鎖扇商,這些永遠(yuǎn)在互相等待的進(jìn)程稱(chēng)為死鎖進(jìn)程。
雖然進(jìn)程在運(yùn)行過(guò)程中宿礁,可能發(fā)生死鎖案铺,但死鎖的發(fā)生也必須具備一定的條件,死鎖的發(fā)生必須具備以下四個(gè)必要條件:
- 互斥條件:指進(jìn)程對(duì)所分配到的資源進(jìn)行排它性使用窘拯,即在一段時(shí)間內(nèi)某資源只由一個(gè)進(jìn)程占用红且。如果此時(shí)還有其它進(jìn)程請(qǐng)求資源,則請(qǐng)求者只能等待涤姊,直至占有資源的進(jìn)程用畢釋放暇番。
- 請(qǐng)求和保持條件:指進(jìn)程已經(jīng)保持至少一個(gè)資源,但又提出了新的資源請(qǐng)求思喊,而該資源已被其它進(jìn)程占有壁酬,此時(shí)請(qǐng)求進(jìn)程阻塞,但又對(duì)自己已獲得的其它資源保持不放。
- 不剝奪條件:指進(jìn)程已獲得的資源舆乔,在未使用完之前岳服,不能被剝奪,只能在使用完時(shí)由自己釋放希俩。
- 環(huán)路等待條件:指在發(fā)生死鎖時(shí)吊宋,必然存在一個(gè)進(jìn)程——資源的環(huán)形鏈,即進(jìn)程集合{P0颜武,P1璃搜,P2,···鳞上,Pn}中的P0正在等待一個(gè)P1占用的資源这吻;P1正在等待P2占用的資源,……篙议,Pn正在等待已被P0占用的資源唾糯。
二、go中鎖機(jī)制
在 Golang 里有專(zhuān)門(mén)的方法來(lái)實(shí)現(xiàn)鎖鬼贱,就是 sync 包移怯,這個(gè)包有兩個(gè)很重要的鎖類(lèi)型。一個(gè)叫 Mutex吩愧, 利用它可以實(shí)現(xiàn)互斥鎖芋酌。一個(gè)叫 RWMutex,利用它可以實(shí)現(xiàn)讀寫(xiě)鎖雁佳。
-
sync.Mutex
的鎖只有一種鎖:Lock()
脐帝,它是互斥鎖,同一時(shí)間只能有一個(gè)鎖糖权。 -
sync.RWMutex
叫讀寫(xiě)鎖堵腹,它有兩種鎖:RLock()
和Lock()
:-
RLock()
叫讀鎖。它不是絕對(duì)鎖星澳,可以有多個(gè)讀者同時(shí)獲取此鎖(調(diào)用mu.RLock
)疚顷。 -
Lock()
叫寫(xiě)鎖,它是個(gè)絕對(duì)鎖禁偎,就是說(shuō)腿堤,如果一旦某人拿到了這個(gè)鎖,別人就不能再獲取此鎖了如暖。
-
1. Mutex-互斥鎖
Mutex 的實(shí)現(xiàn)主要借助了 CAS 指令 + 自旋 + 信號(hào)量
數(shù)據(jù)結(jié)構(gòu):
type Mutex struct {
state int32
sema uint32
}
上述兩個(gè)加起來(lái)只占 8 字節(jié)空間的結(jié)構(gòu)體表示了 Go語(yǔ)言中的互斥鎖
狀態(tài):
在默認(rèn)情況下笆檀,互斥鎖的所有狀態(tài)位都是 0,int32 中的不同位分別表示了不同的狀態(tài):
- 1位表示是否被鎖定
- 1位表示是否有協(xié)程已經(jīng)被喚醒
- 1位表示是否處于饑餓狀態(tài)
- 剩下29位表示阻塞的協(xié)程數(shù)
正常模式和饑餓模式
正常模式:正常模式下waiter都是先入先出盒至,在隊(duì)列中等待的waiter被喚醒后不會(huì)直接獲取鎖酗洒,因?yàn)橐托聛?lái)的goroutine 進(jìn)行競(jìng)爭(zhēng)士修,新來(lái)的goroutine相對(duì)于被喚醒的waiter是具有優(yōu)勢(shì)的,新的goroutine 正在cpu上運(yùn)行樱衷,被喚醒的waiter還要進(jìn)行調(diào)度才能進(jìn)入狀態(tài)棋嘲,所以在并發(fā)的情況下waiter大概率搶不過(guò)新來(lái)的goroutine,這個(gè)時(shí)候waiter會(huì)被放到隊(duì)列的頭部矩桂,如果等待的時(shí)間超過(guò)了1ms沸移,這個(gè)時(shí)候Mutex就會(huì)進(jìn)入饑餓模式。
饑餓模式:當(dāng)Mutex進(jìn)入饑餓模式之后侄榴,鎖的所有權(quán)會(huì)從解鎖的goroutine移交給隊(duì)列頭部的goroutine阔籽,這幾個(gè)時(shí)候新來(lái)的goroutine會(huì)直接放入隊(duì)列的尾部,這樣很好的解決了老的goroutine一直搶不到鎖的場(chǎng)景牲蜀。
對(duì)于兩種模式,正常模式下的性能是最好的绅这,goroutine可以連續(xù)多次獲取鎖涣达,饑餓模式解決了取鎖公平的問(wèn)題,但是性能會(huì)下降证薇,其實(shí)是性能和公平的一個(gè)平衡模式度苔。所以在lock的源碼里面,當(dāng)隊(duì)列只剩本省goroutine一個(gè)并且等待時(shí)間沒(méi)有超過(guò)1ms浑度,這個(gè)時(shí)候Mutex會(huì)重新恢復(fù)到正常模式寇窑。
Lock函數(shù)
// 加鎖
// 如果鎖已經(jīng)被使用,調(diào)用goroutine阻塞箩张,直到鎖可用
func (m *Mutex) Lock() {
// 快速路徑:沒(méi)有競(jìng)爭(zhēng)直接獲取到鎖甩骏,修改狀態(tài)位為加鎖
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
// 開(kāi)啟-race之后會(huì)進(jìn)行判斷,正常情況可忽略
if race.Enabled {
race.Acquire(unsafe.Pointer(m))
}
return
}
// 慢路徑(以便快速路徑可以內(nèi)聯(lián))
m.lockSlow()
}
來(lái)看看Lock函數(shù)先慷,分為兩個(gè)部分饮笛, 快速路徑,先通過(guò)CAS嘗試直接獲取鎖论熙,如果能獲取到直接返回福青,否則進(jìn)入慢路徑的方法,這里的代碼注釋提到了內(nèi)聯(lián)
tips:方法內(nèi)聯(lián)
簡(jiǎn)單的說(shuō)方法內(nèi)聯(lián)就是將被調(diào)用方函數(shù)代碼“復(fù)制”到調(diào)用方函數(shù)中脓诡,減少函數(shù)調(diào)用開(kāi)銷(xiāo)无午,在2018年之前的go版本中,所有的邏輯都在Lock函數(shù)中祝谚,并沒(méi)有拆出來(lái)宪迟,2018年之后Go開(kāi)發(fā)者將slow path拆出來(lái),當(dāng)lock方法被頻繁調(diào)用的時(shí)候踊跟,有兩種情況踩验,如果直接獲得鎖走的是fast path鸥诽,這個(gè)時(shí)候內(nèi)聯(lián)就只有fast path 的代碼,這樣會(huì)減少方法調(diào)用的堆椈叮空間和時(shí)間的消耗 牡借,如果處于自旋,鎖競(jìng)爭(zhēng)的情況下袭异,走的是slow path钠龙,這個(gè)時(shí)候才會(huì)把lock slow 的方法內(nèi)聯(lián)進(jìn)來(lái),這樣方便了編譯器做內(nèi)聯(lián)御铃。
lockSlow 函數(shù)
func (m *Mutex) lockSlow() {
var waitStartTime int64 //記錄請(qǐng)求鎖的初始時(shí)間
starving := false //饑餓標(biāo)記
awoke := false //喚醒標(biāo)記
iter := 0 //自旋次數(shù)
old := m.state //當(dāng)前鎖的狀態(tài)
for {
//鎖處于正常模式還沒(méi)有釋放的時(shí)候碴里,嘗試自旋
if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
//在臨界區(qū)耗時(shí)很短的情況下提高性能
if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0
&& atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
awoke = true
}
runtime_doSpin()
iter++
//更新鎖的狀態(tài)
old = m.state
continue
}
new := old
// 非饑餓狀態(tài)進(jìn)行加鎖
if old&mutexStarving == 0 {
new |= mutexLocked
}
// 等待著數(shù)量+1
if old&(mutexLocked|mutexStarving) != 0 {
new += 1 << mutexWaiterShift
}
// 加鎖的情況下切換為饑餓模式
if starving && old&mutexLocked != 0 {
new |= mutexStarving
}
//goroutine 喚醒的時(shí)候進(jìn)行重置標(biāo)志
if awoke {
if new&mutexWoken == 0 {
throw("sync: inconsistent mutex state")
}
new &^= mutexWoken
}
// 設(shè)置新的狀態(tài)
if atomic.CompareAndSwapInt32(&m.state, old, new) {
if old&(mutexLocked|mutexStarving) == 0 {
break
}
// 判斷是不是第一次加入隊(duì)列
// 如果之前就在隊(duì)列里面等待了,加入到隊(duì)頭
queueLifo := waitStartTime != 0
if waitStartTime == 0 {
waitStartTime = runtime_nanotime()
}
// 阻塞等待
runtime_SemacquireMutex(&m.sema, queueLifo, 1)
// 檢查鎖是否處于饑餓狀態(tài)
starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
old = m.state
// 如果鎖處于饑餓狀態(tài)上真,直接搶到鎖
if old&mutexStarving != 0 {
if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {
throw("sync: inconsistent mutex state")
}
// 設(shè)置標(biāo)志咬腋,進(jìn)行加鎖并且waiter-1
delta := int32(mutexLocked - 1<<mutexWaiterShift)
// 如果是最后一個(gè)的話清除饑餓標(biāo)志
if !starving || old>>mutexWaiterShift == 1 {
// 退出饑餓模式
delta -= mutexStarving
}
atomic.AddInt32(&m.state, delta)
break
}
awoke = true
iter = 0
} else {
old = m.state
}
}
// -race開(kāi)啟檢測(cè)沖突,可以忽略
if race.Enabled {
race.Acquire(unsafe.Pointer(m))
}
}
Unlock函數(shù)
//如果對(duì)沒(méi)有l(wèi)ock 的Mutex進(jìn)行unlock會(huì)報(bào)錯(cuò)
//unlock和goroutine是沒(méi)有綁定的睡互,對(duì)于一個(gè)Mutex根竿,可以一個(gè)goroutine加鎖,另一個(gè)goroutine進(jìn)行解鎖
func (m *Mutex) Unlock() {
if race.Enabled {
_ = m.state
race.Release(unsafe.Pointer(m))
}
// 快速之路就珠,直接解鎖寇壳,去除加鎖位的標(biāo)記
new := atomic.AddInt32(&m.state, -mutexLocked)
if new != 0 {
// 解鎖失敗進(jìn)入慢路徑
//同樣的對(duì)慢路徑做了單獨(dú)封裝,便于內(nèi)聯(lián)
m.unlockSlow(new)
}
}
unlockSlow函數(shù)
func (m *Mutex) unlockSlow(new int32) {
//解鎖一個(gè)未加鎖的Mutex會(huì)報(bào)錯(cuò)(可以想想為什么妻怎,Mutex使用狀態(tài)位進(jìn)行標(biāo)記鎖的狀態(tài)的)
if (new+mutexLocked)&mutexLocked == 0 {
throw("sync: unlock of unlocked mutex")
}
if new&mutexStarving == 0 {
old := new
for {
//正常模式下壳炎,沒(méi)有waiter或者在處理事情的情況下直接返回
if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
return
}
//如果有等待者,設(shè)置mutexWoken標(biāo)志逼侦,waiter-1匿辩,更新state
new = (old - 1<<mutexWaiterShift) | mutexWoken
if atomic.CompareAndSwapInt32(&m.state, old, new) {
runtime_Semrelease(&m.sema, false, 1)
return
}
old = m.state
}
} else {
// 饑餓模式下會(huì)直接將mutex交給下一個(gè)等待的waiter,讓出時(shí)間片榛丢,以便waiter執(zhí)行
runtime_Semrelease(&m.sema, true, 1)
}
}
同樣的撒汉,在unlock也有fastpath和slowpath,fastpath嘗試解鎖涕滋,解鎖成功就返回睬辐,否則進(jìn)入slowpath,slowpath分為正常模式的處理和饑餓模式的處理宾肺,饑餓模式直接將鎖的控制權(quán)交給隊(duì)列中等待的waiter溯饵,正常模式分兩種情況 如果當(dāng)前沒(méi)有waiter,只有自己本身锨用,直接解鎖返回丰刊,如果有waiter,解鎖后喚醒下個(gè)等待者增拥。
2. RWMutex-讀寫(xiě)鎖
RWMutex
是一個(gè)讀/寫(xiě)互斥鎖啄巧,在某一時(shí)刻只能由任意數(shù)量的 reader
持有 或者 一個(gè) writer
持有寻歧。也就是說(shuō),要么放行任意數(shù)量的 reader
秩仆,多個(gè) reader
可以并行讀码泛;要么放行一個(gè) writer
,多個(gè) writer
需要串行寫(xiě)澄耍。
RWMutex 對(duì)外暴露的方法有五個(gè):
-
RLock()
:讀操作獲取鎖噪珊,如果鎖已經(jīng)被 writer 占用,會(huì)一直阻塞直到 writer 釋放鎖齐莲;否則直接獲得鎖痢站; -
RUnlock()
:讀操作完畢之后釋放鎖; -
Lock()
:寫(xiě)操作獲取鎖选酗,如果鎖已經(jīng)被 reader 或者 writer 占用阵难,會(huì)一直阻塞直到獲取到鎖;否則直接獲得鎖芒填; -
Unlock()
:寫(xiě)操作完畢之后釋放鎖多望; -
RLocker()
:返回讀操作的 Locker 對(duì)象,該對(duì)象的 Lock() 方法對(duì)應(yīng) RWMutex 的 -RLock()
氢烘,Unlock() 方法對(duì)應(yīng) RWMutex 的 RUnlock() 方法。
一旦涉及到多個(gè) reader 和 writer 家厌,就需要考慮優(yōu)先級(jí)問(wèn)題播玖,是 reader 優(yōu)先還是 writer 優(yōu)先。
2.1 RWMutex流程概覽
可以想象 RWMutex
有兩個(gè)隊(duì)伍饭于,一個(gè)是包含 所有reader 和你獲得準(zhǔn)入權(quán)writer 的 隊(duì)列A蜀踏,一個(gè)是還沒(méi)有獲得準(zhǔn)入權(quán) writer 的 隊(duì)列B。
隊(duì)列 A 最多只允許有 一個(gè)writer掰吕,如果有其他 writer果覆,需要在 隊(duì)列B 等待;
當(dāng)一個(gè) writer 到了 隊(duì)列A 后殖熟,只允許它 之前的reader 執(zhí)行讀操作局待,新來(lái)的 reader 需要在 隊(duì)列A 后面排隊(duì);
當(dāng)前面的 reader 執(zhí)行完讀操作之后菱属,writer 執(zhí)行寫(xiě)操作钳榨;
writer 執(zhí)行完寫(xiě)操作后,讓 后面的reader 執(zhí)行讀操作纽门,再喚醒隊(duì)列B 的一個(gè) writer 到 隊(duì)列A 后面排隊(duì)薛耻。
初始時(shí)刻 隊(duì)列A 中 writer W1 前面有三個(gè) reader,后面有兩個(gè) reader赏陵,隊(duì)列B中有兩個(gè) writer
并發(fā)讀 多個(gè) reader 可以同時(shí)獲取到讀鎖饼齿,進(jìn)入臨界區(qū)進(jìn)行讀操作饲漾;writer W1 在 隊(duì)列A 中等待,同時(shí)又來(lái)了兩個(gè) reader缕溉,直接在 隊(duì)列A 后面排隊(duì)
寫(xiě)操作 W1 前面所有的 reader 完成后考传,W1 獲得鎖嵌巷,進(jìn)入臨界區(qū)操作
獲得準(zhǔn)入權(quán) W1 完成寫(xiě)操作退出籽暇,先讓后面排隊(duì)的 reader 進(jìn)行讀操作杠河,然后從 隊(duì)列B 中喚醒 W2 到 隊(duì)列A 排隊(duì)墓造。W2 從 隊(duì)列B 到 隊(duì)列A 的過(guò)程中祠斧,R8 先到了 隊(duì)列A形耗,因此 R8 可以執(zhí)行讀操作闻妓。R9双谆、R10返干、R11 在 W2 之后到的兴枯,所以在后面排隊(duì);新來(lái)的 W4 直接在隊(duì)列B 排隊(duì)矩欠。
從上面的示例可以看出财剖,
RWMutex
可以看作是沒(méi)有優(yōu)先級(jí),按照先來(lái)先到的順序去執(zhí)行癌淮,只不過(guò)是 多個(gè)reader 可以 并行去執(zhí)行罷了躺坟。
2.2 寫(xiě)鎖饑餓問(wèn)題
因?yàn)樽x鎖是共享的,所以如果當(dāng)前已經(jīng)有讀鎖乳蓄,那后續(xù)goroutine繼續(xù)加讀鎖正常情況下是可以加鎖成功咪橙,但是如果一直有讀鎖進(jìn)行加鎖,那嘗試加寫(xiě)鎖的goroutine則可能會(huì)長(zhǎng)期獲取不到鎖虚倒,這就是因?yàn)樽x鎖而導(dǎo)致的寫(xiě)鎖饑餓問(wèn)題
go通過(guò)引入以下特性避免出現(xiàn)寫(xiě)鎖饑餓:
- 當(dāng)寫(xiě)鎖阻塞時(shí)美侦,新的讀鎖是無(wú)法申請(qǐng)的
即在sync.RWMutex
的使用中,一個(gè)線程請(qǐng)求了他的寫(xiě)鎖(mx.Lock()
)后魂奥,即便它還沒(méi)有取到該鎖(可能由于資源已被其他人鎖定)菠剩,后面所有的讀鎖的申請(qǐng),都將被阻塞耻煤,只有取寫(xiě)鎖的請(qǐng)求得到了鎖且用完釋放后具壮,讀鎖才能去取。
這種特性可以有效防止寫(xiě)鎖饑餓哈蝇。如果一個(gè)線程因?yàn)槟撤N原因嘴办,導(dǎo)致長(zhǎng)時(shí)間得不到CPU時(shí)間片,這種狀態(tài)被稱(chēng)之為饑餓买鸽。
2.3. golang的讀寫(xiě)鎖源碼剖析
成員變量
結(jié)構(gòu)體
type RWMutex struct {
w Mutex // held if there are pending writers
writerSem uint32 // 用于writer等待讀完成排隊(duì)的信號(hào)量
readerSem uint32 // 用于reader等待寫(xiě)完成排隊(duì)的信號(hào)量
readerCount int32 // 讀鎖的計(jì)數(shù)器
readerWait int32 // 等待讀鎖釋放的數(shù)量
}
寫(xiě)鎖計(jì)數(shù)
讀寫(xiě)鎖中允許加讀鎖的最大數(shù)量是4294967296涧郊,在go里面對(duì)寫(xiě)鎖的計(jì)數(shù)采用了負(fù)值進(jìn)行,通過(guò)遞減最大允許加讀鎖的數(shù)量從而進(jìn)行寫(xiě)鎖對(duì)讀鎖的搶占
const rwmutexMaxReaders = 1 << 30
2.3.1 讀鎖實(shí)現(xiàn)
讀鎖加鎖邏輯
func (rw *RWMutex) RLock() {
if race.Enabled {
_ = rw.w.state
race.Disable()
}
// 累加reader計(jì)數(shù)器眼五,如果小于0則表明有writer正在等待
if atomic.AddInt32(&rw.readerCount, 1) < 0 {
// 當(dāng)前有writer正在等待讀鎖妆艘,讀鎖就加入排隊(duì)
runtime_SemacquireMutex(&rw.readerSem, false)
}
if race.Enabled {
race.Enable()
race.Acquire(unsafe.Pointer(&rw.readerSem))
}
}
讀鎖釋放邏輯
func (rw *RWMutex) RUnlock() {
if race.Enabled {
_ = rw.w.state
race.ReleaseMerge(unsafe.Pointer(&rw.writerSem))
race.Disable()
}
// 如果小于0彤灶,則表明當(dāng)前有writer正在等待
if r := atomic.AddInt32(&rw.readerCount, -1); r < 0 {
if r+1 == 0 || r+1 == -rwmutexMaxReaders {
race.Enable()
throw("sync: RUnlock of unlocked RWMutex")
}
// 將等待reader的計(jì)數(shù)減1,證明當(dāng)前是已經(jīng)有一個(gè)讀的批旺,如果值==0幌陕,則進(jìn)行喚醒等待的
if atomic.AddInt32(&rw.readerWait, -1) == 0 {
// The last reader unblocks the writer.
runtime_Semrelease(&rw.writerSem, false)
}
}
if race.Enabled {
race.Enable()
}
}
2.3.2 寫(xiě)鎖實(shí)現(xiàn)
加寫(xiě)鎖實(shí)現(xiàn)
func (rw *RWMutex) Lock() {
if race.Enabled {
_ = rw.w.state
race.Disable()
}
// 首先獲取mutex鎖,同時(shí)多個(gè)goroutine只有一個(gè)可以進(jìn)入到下面的邏輯
rw.w.Lock()
// 對(duì)readerCounter進(jìn)行進(jìn)行搶占汽煮,通過(guò)遞減rwmutexMaxReaders允許最大讀的數(shù)量
// 來(lái)實(shí)現(xiàn)寫(xiě)鎖對(duì)讀鎖的搶占
r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders
// 記錄需要等待多少個(gè)reader完成,如果發(fā)現(xiàn)不為0搏熄,則表明當(dāng)前有reader正在讀取,當(dāng)前goroutine
// 需要進(jìn)行排隊(duì)等待
if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {
runtime_SemacquireMutex(&rw.writerSem, false)
}
if race.Enabled {
race.Enable()
race.Acquire(unsafe.Pointer(&rw.readerSem))
race.Acquire(unsafe.Pointer(&rw.writerSem))
}
}
釋放寫(xiě)鎖
func (rw *RWMutex) Unlock() {
if race.Enabled {
_ = rw.w.state
race.Release(unsafe.Pointer(&rw.readerSem))
race.Disable()
}
// 將reader計(jì)數(shù)器復(fù)位暇赤,上面減去了一個(gè)rwmutexMaxReaders現(xiàn)在再重新加回去即可復(fù)位
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)
}
// 釋放mutex
rw.w.Unlock()
if race.Enabled {
race.Enable()
}
}
2.3.3 關(guān)鍵核心機(jī)制
寫(xiě)鎖對(duì)讀鎖的搶占
加寫(xiě)鎖的搶占
// 在加寫(xiě)鎖的時(shí)候通過(guò)將readerCount遞減最大允許加讀鎖的數(shù)量心例,來(lái)實(shí)現(xiàn)對(duì)加讀鎖的搶占
r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders
加讀鎖的搶占檢測(cè)
// 如果沒(méi)有寫(xiě)鎖的情況下讀鎖的readerCount進(jìn)行Add后一定是一個(gè)>0的數(shù)字,這里通過(guò)檢測(cè)值為負(fù)數(shù)
//就實(shí)現(xiàn)了讀鎖對(duì)寫(xiě)鎖搶占的檢測(cè)
if atomic.AddInt32(&rw.readerCount, 1) < 0 {
// A writer is pending, wait for it.
runtime_SemacquireMutex(&rw.readerSem, false)
}
寫(xiě)鎖搶占讀鎖后后續(xù)的讀鎖就會(huì)加鎖失敗鞋囊,但是如果想加寫(xiě)鎖成功還要繼續(xù)對(duì)已經(jīng)加讀鎖成功的進(jìn)行等待
if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {
// 寫(xiě)鎖發(fā)現(xiàn)需要等待的讀鎖釋放的數(shù)量不為0止后,就自己自己去休眠了
runtime_SemacquireMutex(&rw.writerSem, false)
}
寫(xiě)鎖既然休眠了,則必定要有一種喚醒機(jī)制其實(shí)就是每次釋放鎖的時(shí)候溜腐,當(dāng)檢查到有加寫(xiě)鎖的情況下译株,就遞減readerWait,并由最后一個(gè)釋放reader lock的goroutine來(lái)實(shí)現(xiàn)喚醒寫(xiě)鎖
if atomic.AddInt32(&rw.readerWait, -1) == 0 {
// The last reader unblocks the writer.
runtime_Semrelease(&rw.writerSem, false)
}
3. 常見(jiàn)問(wèn)題
-
不可復(fù)制
和 Mutex 一樣挺益,RWMutex 也是不可復(fù)制歉糜。不能復(fù)制的原因和互斥鎖一樣。一旦讀寫(xiě)鎖被使用望众,它的字段就會(huì)記錄它當(dāng)前的一些狀態(tài)匪补。這個(gè)時(shí)候你去復(fù)制這把鎖,就會(huì)把它的狀態(tài)也給復(fù)制過(guò)來(lái)黍檩。但是,原來(lái)的鎖在釋放的時(shí)候始锚,并不會(huì)修改你復(fù)制出來(lái)的這個(gè)讀寫(xiě)鎖刽酱,這就會(huì)導(dǎo)致復(fù)制出來(lái)的讀寫(xiě)鎖的狀態(tài)不對(duì),可能永遠(yuǎn)無(wú)法釋放鎖瞧捌。 -
不可重入
不可重入的原因是棵里,獲得鎖之后,還沒(méi)釋放鎖姐呐,又申請(qǐng)鎖殿怜,這樣有可能造成死鎖。比如 reader A 獲取到了讀鎖曙砂,writer B 等待 reader A 釋放鎖头谜,reader 還沒(méi)釋放鎖又申請(qǐng)了一把鎖,但是這把鎖申請(qǐng)不成功鸠澈,他需要等待 writer B柱告。這就形成了一個(gè)循環(huán)等待的死鎖截驮。 - 加鎖和釋放鎖一定要成對(duì)出現(xiàn),不能忘記釋放鎖际度,也不能解鎖一個(gè)未加鎖的鎖葵袭。
References:
https://blog.csdn.net/nrsc272420199/article/details/105032873
https://cloud.tencent.com/developer/article/1700079
https://blog.csdn.net/qq_37935909/article/details/108625508
http://www.reibang.com/p/766093f59687
https://blog.csdn.net/weixin_43580319/article/details/117048449
https://geektutu.com/post/hpg-mutex.html
https://zhuanlan.zhihu.com/p/260205701
https://zhuanlan.zhihu.com/p/98443808
http://www.reibang.com/p/679041bdaa39
https://blog.csdn.net/hudeyong926/article/details/126467118
https://www.cnblogs.com/CJ-cooper/p/15477232.html
https://mp.weixin.qq.com/s?__biz=MzU5NzU2NDk2MA==&mid=2247485176&idx=1&sn=31ca207428c1c8170f714c3771759279&chksm=fe50cdb7c92744a107aaa6eff17e7e62fff3318adfb664234ae2dc6254f3eb57b6c6b491b8d4#rd
https://studygolang.com/topics/15456