Golang 讀寫鎖RWMutex 互斥鎖Mutex 源碼詳解

前言

Golang中有兩種類型的鎖,Mutex (互斥鎖)和RWMutex(讀寫鎖)對於這兩種鎖的使用這裏就很少說了,本文主要側重於從源碼的角度分析這兩種鎖的具體實現。html

 

引子問題

我通常喜歡帶着問題去看源碼。那麼對於讀寫鎖,你是否有這樣的問題,爲何能夠有多個讀鎖?有沒有可能出現有協程一直沒法獲取到寫鎖的狀況?帶着你的疑問來往下看看,具體這個鎖是如何實現的。程序員

若是你本身想看,我給出閱讀的一個思路,能夠先看讀寫鎖,由於讀寫鎖的實現依賴於互斥鎖,而且讀寫鎖比較簡單一些,而後整理思路以後再去想一下實際的應用場景,而後再去看互斥鎖。golang

下面我就會按照這個思路一步步往下走。算法

 

基礎知識點

  • 知識點1:信號量
    信號量是 Edsger Dijkstra 發明的數據結構(沒錯就是那個最短路徑算法那個牛人),在解決多種同步問題時頗有用。其本質是一個整數,並關聯兩個操做:

申請acquire(也稱爲 wait、decrement 或 P 操做)
釋放release(也稱 signal、increment 或 V 操做)數據結構

acquire操做將信號量減 1,若是結果值爲負則線程阻塞,且直到其餘線程進行了信號量累加爲正數才能恢復。如結果爲正數,線程則繼續執行。
release操做將信號量加 1,如存在被阻塞的線程,此時他們中的一個線程將解除阻塞。併發

  • 知識點2:鎖的定義


    在goalng中若是實現了Lock和Unlock方法,那麼它就能夠被稱爲鎖。app

  • 知識點3:鎖的自旋:(詳見百度)函數

  • 知識點4:cas算法:(最好有所瞭解,不知道問題也不大)ui

讀寫鎖RWMutex

首先咱們來看看RWMutex大致結構


看到結構發現讀寫鎖內部包含了一個w Mutex互斥鎖
註釋也很明確,這個鎖的目的就是控制多個寫入操做的併發執行
writerSem是寫入操做的信號量
readerSem是讀操做的信號量
readerCount是當前讀操做的個數
readerWait當前寫入操做須要等待讀操做解鎖的個數
這幾個如今看不懂不要緊,後面等用到了你再回來看就行了。atom

 

而後咱們看看方法


一共有5個方法,看起來就不復雜,咱們一個個來看。


這個最簡單,就是返回一個locker對象沒啥好說的

問題的關鍵就在於鎖和解鎖的幾個方法,由於我已經看過,因此推薦這幾個方法的閱讀順序是RLock Lock RUnlock Unlock

 

RLock(獲取讀鎖)


先不看競態檢測的部分,先重點看紅色框中的部分
能夠看到,其實很簡單,每當有協程須要獲取讀鎖的時候,就將readerCount + 1
可是須要注意的是,這裏有一個條件,當readerCount + 1以後的值 < 0的時候,那麼將會調用runtime_Semacquire方法

這個方法是一個runtime的方法,會一直等待傳入的s出現>0的時候
而後咱們能夠記得,這裏有這樣一個狀況,當出先readerCount + 1爲負數的狀況那麼就會被等待,看註釋咱們能夠猜到,是當有寫入操做出現的時候,那麼讀操做就會被等待。

 

Lock(獲取寫鎖)


寫鎖稍微複雜一些,可是樣子也差很少,咱們仍是先來看紅色框中的部分。
首先操做最前面說的互斥鎖,目的就是處理多個寫鎖併發的狀況,由於咱們知道寫鎖只有一把。這裏不須要深刻互斥鎖,只須要知道,互斥鎖只有一我的能拿到,因此寫鎖只有一我的能拿到。

而後重點來了,這裏的這個操做細細體會一下,atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders)
是將當前的readerCount減去一個很是大的值rwmutexMaxReaders爲1 << 30
大概是1073741823這麼大吧

因此咱們能夠從源碼中看出,readerCount因爲每有一個協程獲取讀鎖就+1,一直都是正數,而當有寫鎖過來的時候,就瞬間減爲很大的負數。
而後作完上面的操做之後的r其實就是原來的readerCount。
後面進行判斷,若是原來的readerCount不爲0(原來有協程已經獲取到了讀鎖)而且將readerWait加上readerCount(表示須要等待readerCount這麼多個讀鎖進行解鎖),若是知足上述條件證實原來有讀鎖,因此暫時沒有辦法獲取到寫鎖,因此調用runtime_Semacquire進行等待,等待的信號量爲writerSem

 

RUnlock(釋放讀鎖)


若是是咱們來寫的話,可能就是將以前+1的readerCount,-1就完事了,可是其實還有一些操做須要注意。
若是-1以後+1==0是啥狀況?沒錯就是咱們常見的,新手程序員,沒有獲取讀鎖就想去釋放讀鎖,因而異常了。固然+1以後恰好是rwmutexMaxReaders,就證獲取了寫鎖而去釋放了讀鎖,致使異常。
除去異常狀況,剩下的就是r仍是<0的狀況,那麼證實確實有協程正在想要獲取寫鎖,那麼就須要操做咱們前面看到的readerWait,當readerWait減到0的時候就證實沒有人正在持有寫鎖了,就經過信號量writerSem的變化告知剛纔等待的協程(想要獲取寫鎖的協程):你能夠進行獲取了。

到這裏你能夠把思路大體串起來了,而後懂了再往下看。

 

Unlock(釋放寫鎖)


寫鎖釋放須要恢復readerCount,還記得上鎖的時候減了一個很大的數,這個時候要加回來了。
固然加完以後若是>=rwmutexMaxReaders自己,那麼仍是新手程序員的問題,當沒有獲取寫鎖的時候就開始想着釋放寫鎖了。
而後for循環就是爲了通知全部在咱們RLock方法中看到的,當有由於持有寫鎖因此等待的那些協程,經過信號量readerSem告訴他們能夠動了。
最後別忘記還有一個互斥鎖須要釋放,讓別的協程也能夠開始搶寫鎖了。

至此,讀寫鎖的分析基本上告一段落了。
針對於其中關於競態分析的代碼,有興趣的小夥伴能夠去了解一下。

 

 

互斥鎖Mutex

互斥鎖比讀寫鎖複雜,可是好在golang給的註釋很詳細,因此也不困難(註釋真的很重要)。
咱們先來看看裏面的一段註釋:

很長的一段英文,我用英語四級的翻譯能力給你翻譯一下,能夠將就看看,若是能夠建議你仔細看英文看懂它,由於這對於後面的源碼閱讀很是重要。


///
這個互斥鎖是公平鎖

互斥鎖有兩種操做模式:正常模式和飢餓模式。
在正常模式下等待獲取鎖的goroutine會以一個先進先出的方式進行排隊,可是被喚醒的等待者並不能表明它已經擁有了這個mutex鎖,它須要與新到達的goroutine爭奪mutex鎖。新來的goroutine有一個優點 —— 他們已經在CPU上運行了而且他們,因此搶到的可能性大一些,因此一個被喚醒的等待者有很大可能搶不過。在這樣的狀況下,被喚醒的等待者在隊列的頭部。若是一個等待者搶鎖超過1ms失敗了,就會切換爲飢餓模式。

在飢餓模式下,mutex鎖會直接由解鎖的goroutine交給隊列頭部的等待者。
新來的goroutine不能嘗試去獲取鎖,即便可能根本就沒goroutine在持有鎖,而且不能嘗試自旋。取而代之的是他們只能排到隊伍尾巴上乖乖等着。

若是一個等待者獲取到了鎖,而且遇到了下面兩種狀況之一,就恢復成正常工做模式。
狀況1:它是最後一個隊列中的等待者。
狀況2:它等待的時間小於1ms

正常模式下,即便有不少阻塞的等待者,有更好的表現,由於一輪能屢次得到鎖的機會。飢餓模式是爲了不那些一直在隊尾的倒黴蛋。
///

 

 

個人話簡單總結就是,互斥鎖有兩種工做模式,競爭模式和隊列模式,競爭就是你們一塊兒搶,隊列就是老老實實排隊,這兩種工做模式會經過一些狀況進行切換。

 

首先仍是來看看大致結構


能夠看到,相對讀寫鎖,結構上面很簡單,只有兩個值,可是千萬不要小瞧它,減小了字段就增長了理解難度。
state:將一個32位整數拆分爲:
當前阻塞的goroutine數(29位)
飢餓狀態(1位,0爲正常模式;1爲飢餓模式)
喚醒狀態(1位,0未喚醒;1已喚醒)
鎖狀態(1位,0可用;1佔用)

sema:信號量


方法也很簡單,就是Lock和Unlock兩個方法,一個上鎖,一個解鎖,沒啥好說的。

 

一個方法

咱們先來看一個的要用到的方法

func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
這個函數,會先判斷參數addr指向的被操做值與參數old的值是否相等,若是相等會將參數new替換參數addr所指向的值,否則的話就啥也不作。
須要特別說明的是,這個方法並不會阻塞。

 

幾個常量

這是定義的幾個常量,咱們在一開始的註釋周圍能夠看到,後面須要用到,暫時記住它們的初始值就好。

mutexLocked = 1 << iota // 1左移0位,是1,二進制是1,(1表示已經上鎖)
mutexWoken // 1左移1位,是2,二進制是10
mutexStarving // 1左移2位,是4,二進制是100
mutexWaiterShift = iota // 就是3, 二進制是11

starvationThresholdNs = 1e6 // 這個就是咱們一開始在註釋裏面看到的1ms,必定超過這個門限值就會更換模式

 

Lock獲取鎖

由於Lock方法比較長,因此我切分一段段看,須要完整的請本身翻看源碼。要注意的一點是,必定要時刻記住,Lock方法是作什麼的,很簡單,就是要搶鎖。看不懂的時候想一想這個目標。

第一步,判斷state狀態是否爲0,若是爲0,證實沒有協程持有鎖,那麼就很簡單了,直接獲取到鎖,將mutexLocked(爲1)賦值到state就能夠了。

看後面的方法時,告訴須要告訴大家一個小技巧,當遇到這種位操做不少的狀況,有兩個方法挺好用,對於你看源碼會有幫助:
第一個是將全部定值先計算,而後判斷非定值的狀況;
第二個是將全部的計算寫下來,本身用筆去計算,不要執着於打字。

而後咱們如下面這個段舉例:

首先,看註釋應該能明白這一段大體意思是,若是不是飢餓模式,就會進行自旋操做,而後不斷循環。

而後根據上面的技巧,old&(mutexLocked|mutexStarving) == mutexLocked
(下面均爲二進制)
mutexLocked = 1
mutexStarving = 11
mutexLocked = 1
這三個是定值,因此咱們容易獲得,知足狀況的結果爲,當old爲xxxx0xx(二進制第三位爲0)等式成立。
也就是咱們一開始說的,state的第三位是表示這個鎖當前的模式,0爲正常模式,1爲飢餓模式。

那麼第一個if就表示,若是當前模式爲正常模式,且能夠自旋,就進入if條件內部。


if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&

一樣的分析,awoke表示是否喚醒,old&mutexWoken是取第二位,0表示當前協程未被喚醒,old>>mutexWaiterShift表示右移3位,也就是前29位,不爲0證實有協程在等待,而且嘗試去對比當前m.state與取出時的old狀態,嘗試去喚醒本身。而後自旋,而且增長自旋次數表示iter,而後從新賦值old。再循環下一次。

(你本身理一理,確實有點繞,仔細想一想就想通了就對了。)

以上是使用自旋的狀況,就是canSpin的。


而後進行判斷old&mutexStarving == 0就是第三位爲0的狀況,仍是所說的正常模式。new就立刻拿到鎖了,new |= mutexLocked,表示或1,就是第一位不管是啥都賦值爲1

 

old&(mutexLocked|mutexStarving),也就是old & 0101
必須當old的1和3兩個位置爲1的時候纔是true,也就是說當前處於飢餓模式,而且鎖已經被佔用的狀況,那麼就須要排隊去。
排隊也很精妙,new += 1 << mutexWaiterShift
這邊注意是先計算1 << mutexWaiterShift也就是將new的前29位+1,就是表示有一個協程在等待了。

 

好了到這裏你的位操做應該就習慣的差很少了,以後我就直接說結論,不仔細的幫你01表示了,你已經長大了,要學會本身動手了。

若是當前已經標記爲飢餓模式,而且沒有鎖住,那麼設置new爲飢餓模式
if starving && old&mutexLocked != 0 {
new |= mutexStarving
}

 

若是喚醒,須要在兩種狀況下重設標誌
if awoke {
若是喚醒標誌爲與awoke不相協調就panic
if new&mutexWoken == 0 {
throw("sync: inconsistent mutex state")
}
設置喚醒狀態位0,被喚醒
new &= mutexWoken
}


若是獲取鎖成功

old&(mutexLocked|mutexStarving) == 0成立表示已經獲取鎖,就直接退出CAS

中間這一段我就很少解釋了,就是最前面註釋說的,知足什麼條件轉換什麼模式,很少說了。而後從隊列中,也就是前29位-1。
須要注意其中有一個runtime_SemacquireMutex和以前看的的runtime_Semacquire是一個意思,只是多了一個參數。

這個就是這個方法的註釋。能夠看到,就是多了個隊列去排隊。


若是獲取鎖失敗,old刷新狀態再次循環,繼續cas

 

UnLock釋放鎖

Unlock就相對簡單一些,競態分析不看。
其實咱們本身想也能想到,unlock就是將標識位改回來嘛。
而後由於咱們已經看過讀寫鎖了,也是一樣的道理,若是沒有上鎖就直接解鎖,那確定報錯嘛。


而後若是是正常模式,若是沒有等待的goroutine或goroutine已經解鎖完成的狀況就直接返回了。若是有等待的goroutine那就經過信號量去喚醒runtime_Semrelease(注意這裏是false),同時操做一下隊列-1


若是是飢餓模式就直接喚醒(注意這裏是true),反正有隊列嘛。

 

總結

其實話說回來,咱們其實看起來也簡單,沒有衝突的狀況下,能拿就拿唄,若是出現衝突了就嘗試自旋解決(自旋通常都能解決)若是解決不了就經過信號量解決,同時若是正常模式就是咱們說的搶佔式,非公平,若是是飢餓模式,就是咱們說的排隊,公平,防止有一些倒黴蛋一直搶不到。

總體總結一下,看完源碼咱們發現,其實鎖的設計並不複雜,主要設計咱們要學到cas和處理讀寫狀態的信號量通知,對於那些位操做,能看懂,學可能一時半會學不會,由於很難在一開始就設計的那麼巧妙,你也體會到了只用一個變量就維護了整個體系是一種藝術。

 寫的着急,不免有疏漏,若是有任何問題請評論,立刻修改,以避免誤導。

 

 

 

 

做者:LinkinStar
未經容許,不得轉載
出處:http://www.javashuo.com/article/p-eapjpdem-md.html

相關文章
相關標籤/搜索