golang筆記——深入了解go中鎖機(jī)制

一绘沉、鎖的基礎(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)題:

  1. 試圖遞歸地獲得自旋鎖必然會(huì)引起死鎖:遞歸程序的持有實(shí)例在第二個(gè)實(shí)例循環(huán),以試圖獲得相同自旋鎖時(shí)担锤,不會(huì)釋放此自旋鎖蔚晨。
    在遞歸程序中使用自旋鎖應(yīng)遵守下列策略:遞歸程序決不能在持有自旋鎖時(shí)調(diào)用它自己,也決不能在遞歸調(diào)用時(shí)試圖獲得相同的自旋鎖。

  2. 過(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)題
  1. 不可復(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ú)法釋放鎖瞧捌。
  2. 不可重入
    不可重入的原因是棵里,獲得鎖之后,還沒(méi)釋放鎖姐呐,又申請(qǐng)鎖殿怜,這樣有可能造成死鎖。比如 reader A 獲取到了讀鎖曙砂,writer B 等待 reader A 釋放鎖头谜,reader 還沒(méi)釋放鎖又申請(qǐng)了一把鎖,但是這把鎖申請(qǐng)不成功鸠澈,他需要等待 writer B柱告。這就形成了一個(gè)循環(huán)等待的死鎖截驮。
  3. 加鎖和釋放鎖一定要成對(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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市乖菱,隨后出現(xiàn)的幾起案子坡锡,更是在濱河造成了極大的恐慌,老刑警劉巖窒所,帶你破解...
    沈念sama閱讀 210,978評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鹉勒,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡墩新,警方通過(guò)查閱死者的電腦和手機(jī)贸弥,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)海渊,“玉大人绵疲,你說(shuō)我怎么就攤上這事〕家桑” “怎么了盔憨?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,623評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)讯沈。 經(jīng)常有香客問(wèn)我郁岩,道長(zhǎng),這世上最難降的妖魔是什么缺狠? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,324評(píng)論 1 282
  • 正文 為了忘掉前任问慎,我火速辦了婚禮,結(jié)果婚禮上挤茄,老公的妹妹穿的比我還像新娘如叼。我一直安慰自己,他們只是感情好穷劈,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,390評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布笼恰。 她就那樣靜靜地躺著,像睡著了一般歇终。 火紅的嫁衣襯著肌膚如雪社证。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,741評(píng)論 1 289
  • 那天评凝,我揣著相機(jī)與錄音追葡,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛辽俗,可吹牛的內(nèi)容都是我干的疾渣。 我是一名探鬼主播,決...
    沈念sama閱讀 38,892評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼崖飘,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼榴捡!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起朱浴,我...
    開(kāi)封第一講書(shū)人閱讀 37,655評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤吊圾,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后翰蠢,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體项乒,經(jīng)...
    沈念sama閱讀 44,104評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年梁沧,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了檀何。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,569評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡廷支,死狀恐怖频鉴,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情恋拍,我是刑警寧澤垛孔,帶...
    沈念sama閱讀 34,254評(píng)論 4 328
  • 正文 年R本政府宣布,位于F島的核電站施敢,受9級(jí)特大地震影響周荐,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜僵娃,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,834評(píng)論 3 312
  • 文/蒙蒙 一概作、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧默怨,春花似錦讯榕、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,725評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)谆甜。三九已至垃僚,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間规辱,已是汗流浹背谆棺。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,950評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人改淑。 一個(gè)月前我還...
    沈念sama閱讀 46,260評(píng)論 2 360
  • 正文 我出身青樓碍岔,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親朵夏。 傳聞我的和親對(duì)象是個(gè)殘疾皇子蔼啦,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,446評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容

  • 同步原語(yǔ)和鎖 Golang作為一個(gè)原生支持用戶態(tài)的語(yǔ)言,當(dāng)提到并發(fā)進(jìn)程仰猖,多線程的時(shí)候捏肢,是離不開(kāi)鎖的,鎖是一種并發(fā)編...
    LegendGo閱讀 2,227評(píng)論 0 3
  • go并發(fā)編程入門(mén)到放棄 并發(fā)和并行 并發(fā):一個(gè)處理器同時(shí)處理多個(gè)任務(wù)饥侵。 并行:多個(gè)處理器或者是多核的處理器同時(shí)處理...
    yangyunfeng閱讀 555評(píng)論 0 2
  • 前言 Golang中有兩種類(lèi)型的鎖鸵赫,Mutex (互斥鎖)和RWMutex(讀寫(xiě)鎖)對(duì)于這兩種鎖的使用這里就不多說(shuō)...
    LinkinStar閱讀 5,754評(píng)論 2 3
  • 轉(zhuǎn)載自:https://halfrost.com/go_map_chapter_one/ https://half...
    HuJay閱讀 6,131評(píng)論 1 5
  • Golang中的鎖機(jī)制主要包含互斥鎖和讀寫(xiě)鎖 互斥鎖 互斥鎖是一種簡(jiǎn)單的加鎖的方法來(lái)控制對(duì)共享資源的訪問(wèn),互斥鎖只...
    林桉閱讀 1,771評(píng)論 0 0