什么是鎖,為什么使用鎖
用俗語來說,鎖意味着一種保護,對資源的一種保護,在程序員眼中,這個資源可以是一個變量,一個代碼片段,一條記錄,一張數據庫表等等。
就跟小孩需要保護一樣,不保護的話小孩會收到傷害,同樣的使用鎖的原因是資源不保護的話,可能會受到污染,在並發情況下,多個人對同一資源進行操作,有可能導致資源不符合預期的修改。
常見的鎖的種類
鎖的種類細分的話,非常多,主要原因是從不同角度看,對鎖的定義不一樣,我這里總結了一下,畫一個思維腦圖,大家了解一下。
我個人認為鎖都可以歸為一下四大類,其它的叫法不同只是因為其實現方式或者應用場景而得名,但本質上上還是下面的這四大類中一種。
其它各種類的鎖總結如下,這些鎖只是為了高性能,為了各種應用場景在代碼實現上做了很多工作,因此而得名,關於他們的資料很多
更多鎖的詳細解釋參考我github的名詞描述,這里不在贅述,地址如下:
https://github.com/sunpengwei1992/java_common/tree/master/src/lock
Go中的鎖使用和實現分析
Go的代碼庫中為開發人員提供了一下兩種鎖:
- 互斥鎖 sync.Mutex
- 讀寫鎖 sync.RWMutex
第一個互斥鎖指的是在Go編程中,同一資源的鎖定對各個協程是相互排斥的,當其中一個協程獲取到該鎖時,其它協程只能等待,直到這個獲取鎖的協程釋放鎖之后,其它的協程才能獲取。
第二個讀寫鎖依賴於互斥鎖的實現,這個指的是當多個協程對某一個資源都是只讀操作,那么多個協程可以獲取該資源的讀鎖,並且互相不影響,但當有協程要修改該資源時就必須獲取寫鎖,如果獲取寫鎖時,已經有其它協程獲取了讀寫或者寫鎖,那么此次獲取失敗,也就是說讀寫互斥,讀讀共享,寫寫互斥。
Go中關於鎖的接口定義如下:,該接口的實現就是上面的兩個鎖種類,篇幅有限,這篇文章主要是分析一下互斥鎖的使用和實現,因為RWMutex也是基於Mutex的,大家可以參考文章自行學習一下。
type Locker interface {
Lock()
Unlock()
}
type Mutex struct {
state int32 //初始值默認為0
sema uint32 //初始值默認為0
}
Mutex使用也非常的簡單,,聲明一個Mutex變量就可以直接調用Lock和Unlock方法了,如下代碼實例,但使用的過程中有一些注意點,如下:
- 同一個協程不能連續多次調用Lock,否則發生死鎖
- 鎖資源時盡量縮小資源的范圍,以免引起其它協程超長時間等待
- mutex傳遞給外部的時候需要傳指針,不然就是實例的拷貝,會引起鎖失敗
- 善用defer確保在函數內釋放了鎖
- 使用-race在運行時檢測數據競爭問題,go test -race ....,go build -race ....
- 善用靜態工具檢查鎖的使用問題
- 使用go-deadlock檢測死鎖,和指定鎖超時的等待問題(自己百度工具用法)
- 能用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的源碼較長,我將注釋寫入代碼中,方便大家理解,整個鎖的過程其實分為三部分,建議大家參考源碼和我的注釋一塊學習。
- 直接獲取鎖,返回
- 自旋和喚醒
- 判斷各種狀態,特殊情況處理
第一部分代碼如下,較為簡單,獲取鎖成功之后直接返回
//對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的源碼並不是很復雜,只是各種位運算讓開發人員難以直接觀察到結果值,另外閱讀源碼前一定要先明白各個變量和常量的含義,不然讀起來非常費勁。
