Go中鎖的那些姿勢,估計你不知道


什么是鎖,為什么使用鎖

用俗語來說,鎖意味着一種保護,對資源的一種保護,在程序員眼中,這個資源可以是一個變量,一個代碼片段,一條記錄,一張數據庫表等等。

就跟小孩需要保護一樣,不保護的話小孩會收到傷害,同樣的使用鎖的原因是資源不保護的話,可能會受到污染,在並發情況下,多個人對同一資源進行操作,有可能導致資源不符合預期的修改。

常見的鎖的種類

鎖的種類細分的話,非常多,主要原因是從不同角度看,對鎖的定義不一樣,我這里總結了一下,畫一個思維腦圖,大家了解一下。

我個人認為鎖都可以歸為一下四大類,其它的叫法不同只是因為其實現方式或者應用場景而得名,但本質上上還是下面的這四大類中一種。

其它各種類的鎖總結如下,這些鎖只是為了高性能,為了各種應用場景在代碼實現上做了很多工作,因此而得名,關於他們的資料很多

更多鎖的詳細解釋參考我github的名詞描述,這里不在贅述,地址如下:

https://github.com/sunpengwei1992/java_common/tree/master/src/lock

Go中的鎖使用和實現分析

Go的代碼庫中為開發人員提供了一下兩種鎖:

  1. 互斥鎖 sync.Mutex
  2. 讀寫鎖 sync.RWMutex

第一個互斥鎖指的是在Go編程中,同一資源的鎖定對各個協程是相互排斥的,當其中一個協程獲取到該鎖時,其它協程只能等待,直到這個獲取鎖的協程釋放鎖之后,其它的協程才能獲取。

第二個讀寫鎖依賴於互斥鎖的實現,這個指的是當多個協程對某一個資源都是只讀操作,那么多個協程可以獲取該資源的讀鎖,並且互相不影響,但當有協程要修改該資源時就必須獲取寫鎖,如果獲取寫鎖時,已經有其它協程獲取了讀寫或者寫鎖,那么此次獲取失敗,也就是說讀寫互斥,讀讀共享,寫寫互斥。

Go中關於鎖的接口定義如下:,該接口的實現就是上面的兩個鎖種類,篇幅有限,這篇文章主要是分析一下互斥鎖的使用和實現,因為RWMutex也是基於Mutex的,大家可以參考文章自行學習一下。

type Locker interface {
   Lock()
   Unlock()
}
type Mutex struct {
   state int32 //初始值默認為0
   sema  uint32 //初始值默認為0
}

Mutex使用也非常的簡單,,聲明一個Mutex變量就可以直接調用Lock和Unlock方法了,如下代碼實例,但使用的過程中有一些注意點,如下:

  1. 同一個協程不能連續多次調用Lock,否則發生死鎖
  2. 鎖資源時盡量縮小資源的范圍,以免引起其它協程超長時間等待
  3. mutex傳遞給外部的時候需要傳指針,不然就是實例的拷貝,會引起鎖失敗
  4. 善用defer確保在函數內釋放了鎖
  5. 使用-race在運行時檢測數據競爭問題,go test -race ....,go build -race ....
  6. 善用靜態工具檢查鎖的使用問題
  7. 使用go-deadlock檢測死鎖,和指定鎖超時的等待問題(自己百度工具用法)
  8. 能用channel的場景別使用成了lock
var lock sync.Mutex

func MutexStudy(){
    //獲取鎖
    lock.Lock()
    //業務邏輯操作
    time.Sleep(1 * time.Second)
    //釋放鎖
    defer lock.Unlock()
}

我們了解了Mutext的使用和注意事項,那么具體原理是怎么實現的呢?運用到了那些技術,下面一起分析一下Mutex的實現原理。

Mutex實現中有兩種模式,1:正常模式,2:飢餓模式,前者指的是當一個協程獲取到鎖時,后面的協程會排隊(FIFO),釋放鎖時會喚醒最早排隊的協程,這個協程會和正在CPU上運行的協程競爭鎖,但是大概率會失敗,為什么呢?因為你是剛被喚醒的,還沒有獲得CPU的使用權,而CPU正在執行的協程肯定比你有優勢,如果這個被喚醒的協程競爭失敗,並且超過了1ms,那么就會退回到后者(飢餓模式),這種模式下,該協程在下次獲取鎖時直接得到,不存在競爭關系,本質是為了防止協程等待鎖的時間太長。

兩種模式都了解了,我們再來分析一下幾個核心常量,代碼如下:

const (
   mutexLocked = 1 << iota //1, 0001 最后一位表示當前鎖的狀態,0未鎖,1已鎖 
   mutexWoken //2, 0010,倒數第二位表示當前鎖是否會被喚醒,0喚醒,1未喚醒
   mutexStarving //4, 0100 倒數第三位表示當前對象是否為飢餓模式,0正常,1飢餓
   mutexWaiterShift = iota //3 從倒數第四位往前的bit表示排隊的gorouting數量
   starvationThresholdNs = 1e6 // 飢餓的閾值:1ms
)
//Mutex中的變量,這里主要是將常量映射到state上面
state //0代表未獲取到鎖,1代表得到鎖,2-2^31表示gorouting排隊的數量的
sema //非負數的信號量,阻塞協程的依據

這幾個變量你要是都弄白了,那么代碼看起來就相對好理解一些了,整個Lock的源碼較長,我將注釋寫入代碼中,方便大家理解,整個鎖的過程其實分為三部分,建議大家參考源碼和我的注釋一塊學習。

  1. 直接獲取鎖,返回
  2. 自旋和喚醒
  3. 判斷各種狀態,特殊情況處理

第一部分代碼如下,較為簡單,獲取鎖成功之后直接返回

//對state進行cas修改操作,修改成功相當於獲取鎖,修改之后state=1
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
	return
}

第二部分自旋的代碼如下

//開始等待時間
var waitStartTime int64
//這幾個變量含義依次是:是否飢餓,是否喚醒,自旋次數,鎖的當前狀態
starving := false;awoke := false;iter := 0;old := m.state
//進入死循環,直到獲得鎖成功(獲得鎖成功就是有別的協程釋放鎖了)
for {
    //這個if的核心邏輯是判斷:已經獲得鎖了並且不是飢餓模式 && 可以自旋,與cpu核數有關
	if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
           //這個是判斷:沒有被喚醒 && 有排隊等待的協程 && 嘗試設置通知被喚醒
		if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 && atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
           //說明上個協程此時已經unlock了,喚醒當前協程
			awoke = true
		}
        //自旋一段時間
		runtime_doSpin()
        //自選次數加1
		iter++
		old = m.state
		continue
	}
}
        

第三部分代碼,判斷各種狀態,特殊情況處理

new := old
 //1:原協程已經unlock了,對new的修改為已鎖
if old&mutexStarving == 0 { 
	new |= mutexLocked
}
//2:這里是執行完自旋或者沒執行自旋(原協程沒有unlock)
if old&(mutexLocked|mutexStarving) != 0 {
	new += 1 << mutexWaiterShift //排隊
}
//3:如果是飢餓模式,並且已鎖的狀態
if starving && old&mutexLocked != 0 {
	new |= mutexStarving //設置new為飢餓狀態
}
 //4:上面的awoke被設置為true
if awoke {
    //當前協程被喚醒了,肯定不為0
    if new&mutexWoken == 0 {
	    throw("sync: inconsistent mutex state")
    }
    //既然當前協程被喚醒了,重置喚醒標志為0
    new &^= mutexWoken
}
//修改state的值為new,但這里new的值會有四種情況,
//就是上面4個if情況對new做的修改,這一步獲取鎖成功
if atomic.CompareAndSwapInt32(&m.state, old, new) {
	if old&(mutexLocked|mutexStarving) == 0 {
        //這里代表的是正常模式獲取鎖成功
        break 
	}
    //下面的代碼是判斷是否從飢餓模式恢復正常模式 
	queueLifo := waitStartTime != 0
	if waitStartTime == 0 {
		waitStartTime = runtime_nanotime()
	}
   //進入阻塞狀態  
	runtime_SemacquireMutex(&m.sema, queueLifo)
   //設置是否為飢餓模式,等待的時間大於1ms就是飢餓模式  
	starving=starving||runtime_nanotime()-waitStartTime> starvationThresholdNs
	old = m.state
    //如果當前鎖是飢餓模式,但這個gorouting被喚醒
	if old&mutexStarving != 0 {
		if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {
					throw("sync: inconsistent mutex state")
		}
       //減去當前鎖的排隊
		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
}

Lock的源碼我們弄明白了,那么Unlock呢,大家看代碼的時候最好Lock和Unlock結合一起來看,因為他們是對同一變量state在操作

func (m *Mutex) Unlock() {
   //釋放鎖
   new := atomic.AddInt32(&m.state, -mutexLocked)
   if (new+mutexLocked)&mutexLocked == 0 {
      throw("sync: unlock of unlocked mutex")
   }
   //判斷當前鎖是否飢餓模式,==0代表不是
   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)
            return
         }
         old = m.state
      }
   } else {
      //釋放信號量
      runtime_Semrelease(&m.sema, true)
   }
}

到這里整個Mutex的源碼分析完成,可以看到Metux的源碼並不是很復雜,只是各種位運算讓開發人員難以直接觀察到結果值,另外閱讀源碼前一定要先明白各個變量和常量的含義,不然讀起來非常費勁。

![](https://img2018.cnblogs.com/blog/706455/202001/706455-20200113092119426-247247567.jpg)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM