golang中的Mutex設計原理詳解(一)
Mutex系列是根據我對晁嶽攀老師的《Go 併發程式設計實戰課》的吸收和理解整理而成,如有偏差,歡迎指正~
目標
本系列除了希望徹底學習和了解 golang 中 sync.Mutex 的原理和使用,更希望借 golang 中 Mutex 的發展和演變,瞭解併發場景下鎖的設計與實現方法以及不通業務場景下的一些特殊考慮。
Mutex 簡介
Mutex 是什麼
Mutex 是 golang 標準庫的互斥鎖,主要用來處理併發場景下共享資源的訪問衝突問題。
Mutex 定義
儘管 Mutex 的實現經歷了多次的重大改版,但是因為設計的巧妙,使用上並沒有發生任何變化。
package sync // import "sync"
type Mutex struct {
state int32
sema uint32
}
A Mutex is a mutual exclusion lock. The zero value for a Mutex is an
unlocked mutex.
A Mutex must not be copied after first use.
func (m *Mutex) Lock()
func (m *Mutex) Unlock()
從 Mutex 的定義一眼就能看出來如何使用,加鎖使用 Lock 函式,解鎖使用 Unlock 函式。
其實 package sync
中定義了 Locker 介面:
package sync // import "sync"
type Locker interface {
Lock()
Unlock()
}
A Locker represents an object that can be locked and unlocked.
Mutex 實現了 Locker 介面。除了互斥鎖 Mutex,像之後會介紹的讀寫鎖 RWMutex,也實現了 Locker 介面。
golang 中 Mutex 演變的4個階段
現在去看 go1.14 中 Mutex 的實現,是比較複雜和精巧的,但是 Mutex 的複雜和精巧不是一蹴而就的。從初版的 Mutex,到現在的 Mutex,大致經過了以下4個階段的演變:
接下來會通過這4個階段對應的 Mutex 原始碼來理解 golang 在互斥鎖的設計思路上的逐漸進化的過程。
希望通過這樣一個學習,不僅能更好的掌握 Mutex 這個工具,還能學習到如何設計一個兼顧公平和效能的互斥鎖。
初版 Mutex 實現
初版 Mutex 的具體實現如下:
// CAS操作,當時還沒有抽象出atomic包
func cas(val *int32, old, new int32) bool
func semacquire(*int32)
func semrelease(*int32)
// 互斥鎖的結構,包含兩個欄位
type Mutex struct {
key int32 // 鎖是否被持有的標識
sema int32 // 訊號量專用,用以阻塞/喚醒goroutine
}
// 保證成功在val上增加delta的值
func xadd(val *int32, delta int32) (new int32) {
for {
v := *val
if cas(val, v, v+delta) {
return v + delta
}
}
panic("unreached")
}
// 請求鎖
func (m *Mutex) Lock() {
if xadd(&m.key, 1) == 1 { //標識加1,如果等於1,成功獲取到鎖
return
}
semacquire(&m.sema) // 否則阻塞等待
}
func (m *Mutex) Unlock() {
if xadd(&m.key, -1) == 0 { // 將標識減去1,如果等於0,則沒有其它等待者
return
}
semrelease(&m.sema) // 喚醒其它阻塞的goroutine
}
理解程式碼之前,先簡單介紹下 cas、semacquire 和 semrelease。
cas 的全拼是 compare and set
或者 compare and swap
。cas 指令實現的功能是將給定的值 old 和記憶體中的值 *val 比較,如果相等,將 new 賦值給 *val,否則返回失敗。
semacquire 和 semrelease 利用訊號量 sema 實現了阻塞和喚醒功能。
接下來我們開始分析上面的程式碼。
初版 Mutex 的定義
首先看 Mutex 的定義。這個初版的定義其實和最新版的定義的區別在 key 這個欄位上。初版中,key 的含義比較簡單,就是一個標誌位,等於0表示鎖未被持有,1表示被某個 goroutine 持有,等於 n 表示還有 n-1 個等待者。
加鎖
加鎖(Lock)的過程首先是給 key 加1。
如果 key 返回1,則表示當前 goroutine 佔有了這把鎖,其它 goroutine 只能做候選者。
如果 key 返回n(n > 1),這說明當前有其它 gorutine 正在佔用這把鎖,所以接下來需要通過訊號量機制將當前 goroutine 掛起,加到等待佇列,進入阻塞狀態。
解鎖
解鎖(Unlock)的過程是給 key 減1。
如果 key 返回0,表示當前沒有其它 goroutine 在等待,可以直接返回;如果 key 返回 n (n > 0),說明還有其它 goroutine 在等待,因此需要通過訊號量機制將等待佇列中的其它 goroutine 喚醒。
初版 Mutex 的問題
初版 Mutex 在實現的時候,有兩個問題:1)Unlock 呼叫無限制;2)goroutine 喚醒機制效能低下。
Unlock 呼叫無限制問題
Mutex 本身並沒有包含當前 goroutine 的任何資訊,因此 Unlock 方法能被任意的 goroutine 呼叫。這樣會導致一個問題,如果某個 goroutine 不按套路來,隨便呼叫 Unlock 函式,讓標誌位 key 清零,那麼資料競爭的問題還是會出現。
Mutex 的這個特性一直保留至今。因此使用 Mutex 的時候,一定要遵循 “誰加鎖,誰解鎖” 的原則。
goroutine 喚醒機制效能低下
初版 Mutex 喚醒 goroutine 的機制是按排隊順序,誰在前面就先喚醒誰。這樣看著很公平,但是從效能上看,並不是最優。因為沉睡的 goroutine 喚醒之後,還需要進行上下文的切換,如果把喚醒機會給當前正佔用 CPU 時間片的 goroutine,那麼高併發的時候,可能會有更好的效能。
這也是下一個版本的 Mutex 重點解決的問題。
結尾
初版的 Mutex 通過 標誌位 key 實現了互斥鎖的基本功能。在下一個版本的 Mutex 中,我會重點闡述 Mutex 是如何解決 goroutine 喚醒機制效能低下的問題。
附
1、初版 Mutex 的詳細程式碼
2、初版 cas 的詳細程式碼
3、初版 semacquire 和 semrelease 詳細程式碼
相關文章
- golang pprof 監控系列(3) —— memory,block,mutex 統計原理GolangBloCMutex
- golang 中 sync.Mutex 的實現GolangMutex
- Golang 讀寫鎖RWMutex 互斥鎖Mutex 原始碼詳解GolangMutex原始碼
- 原始碼剖析 golang 中 sync.Mutex原始碼GolangMutex
- PHP 中的設計模式詳解PHP設計模式
- Java中的設計模式詳解Java設計模式
- 併發程式設計 — CAS 原理詳解程式設計
- Go 語言併發程式設計之互斥鎖詳解 sync.MutexGo程式設計Mutex
- 詳解數倉物件設計中序列SEQUENCE原理與應用物件
- golang中的socket程式設計Golang程式設計
- GoLang協程Goroutiney原理與GMP模型詳解Golang模型
- CTMediator 原理詳解(一)
- Golang make和new的區別及實現原理詳解Golang
- Golang WaitGroup 底層原理及原始碼詳解GolangAI原始碼
- SAP ABAP報表依賴設計原理詳解
- [譯] Part 32: 詳解Golang 中的Panic 和 RecoverGolang
- Golang中的強大Web框架Fiber詳解GolangWeb框架
- 詳解APP介面設計中的微妙細節APP
- Golang環境變數設定詳解Golang變數
- 泛型程式設計詳解(一)泛型程式設計
- KVC/KVO原理詳解及程式設計指南(轉載)程式設計
- Linux shell程式設計(一)shell指令碼中的變數詳解Linux程式設計指令碼變數
- Golang併發程式設計中select簡單瞭解Golang程式設計
- golang開發:CSP-WaitGroup MutexGolangAIMutex
- 詳解Java 容器(第②篇)——容器中的設計模式Java設計模式
- 詳解MapReduce中的五大程式設計模型程式設計模型
- Python 中的設計模式詳解之:策略模式Python設計模式
- Go 併發程式設計之 MutexGo程式設計Mutex
- Go併發程式設計--Mutex/RWMutexGo程式設計Mutex
- c#中建造者設計模式詳解C#設計模式
- MySQL中count(*)函式原理詳解MySql函式
- Java網路程式設計和NIO詳解6:Linux epoll實現原理詳解Java程式設計Linux
- 一文掌握Golang中IO包使用與原理Golang
- Golang通道Channel詳解Golang
- golang-channel詳解Golang
- golang select詳解Golang
- [譯] part25: golang Mutex互斥鎖GolangMutex
- go中sync.Mutex原始碼解讀GoMutex原始碼