一文帶你深入理解Golang中的RWMutex
在上一篇文章《深入理解 go Mutex》中, 我們已經(jīng)對 go Mutex 的實現(xiàn)原理有了一個大致的了解,也知道了 Mutex 可以實現(xiàn)并發(fā)讀寫的安全。 今天,我們再來看看另外一種鎖,RWMutex,有時候,其實我們讀數(shù)據(jù)的頻率要遠(yuǎn)遠(yuǎn)高于寫數(shù)據(jù)的頻率, 而且不同協(xié)程應(yīng)該可以同時讀取的,這個時候,RWMutex 就派上用場了。
RWMutex 的實現(xiàn)原理和 Mutex 類似,只是在 Mutex 的基礎(chǔ)上,區(qū)分了讀鎖和寫鎖:
- 讀鎖:只要沒有寫鎖,就可以獲取讀鎖,多個協(xié)程可以同時獲取讀鎖(可以并行讀)。
- 寫鎖:只能有一個協(xié)程獲取寫鎖,其他協(xié)程想獲取讀鎖或?qū)戞i都只能等待。
下面就讓我們來深入了解一下 RWMutex 的基本使用和實現(xiàn)原理等內(nèi)容。
RWMutex 的整體模型
正如 RWMutex 的命名那樣,它是區(qū)分了讀鎖和寫鎖的鎖,所以我們可以從讀和寫兩個方面來看 RWMutex 的模型。
下文中的 reader 指的是進(jìn)行讀操作的 goroutine,writer 指的是進(jìn)行寫操作的 goroutine。
讀操作模型
我們可以用下圖來表示 RWMutex 的讀操作模型:

上圖使用了 w.Lock,是因為 RWMutex 的實現(xiàn)中,寫鎖是使用 Mutex 來實現(xiàn)的。
說明:
- 讀操作的時候可以同時有多個 goroutine 持有
RLock,然后進(jìn)入臨界區(qū)。(也就是可以并行讀),上圖的G1、G2和G3就是同時持有RLock的幾個 goroutine。 - 在讀操作的時候,如果有 goroutine 持有
RLock,那么其他 goroutine (不管是讀還是寫)就只能等待,直到所有持有RLock的 goroutine 釋放鎖。 - 也就是上圖的
G4需要等待G1、G2和G3釋放鎖之后才能進(jìn)入臨界區(qū)。 - 最后,因為
G5和G6這兩個協(xié)程獲取鎖的時機比G4晚,所以它們會在G4釋放鎖之后才能進(jìn)入臨界區(qū)。
寫操作模型
我們可以用下圖來表示 RWMutex 的寫操作模型:

說明:
寫操作的時候只能有一個 goroutine 持有 Lock,然后進(jìn)入臨界區(qū),釋放寫鎖之前,所有其他的 goroutine 都只能等待。
上圖的 G1~G5 表示的是按時間順序先后獲取鎖的幾個 goroutine。
上面幾個 goroutine 獲取鎖的過程是:
G1獲取寫鎖,進(jìn)入臨界區(qū)。然后G2、G3、G4和G5都在等待。G1釋放寫鎖之后,G2和G3可以同時獲取讀鎖,進(jìn)入臨界區(qū)。然后G3、G4和G5都在等待。G2和G3可以同時獲取讀鎖,進(jìn)入臨界區(qū)。然后G4和G5都在等待。G2和G3釋放讀鎖之后,G4獲取寫鎖,進(jìn)入臨界區(qū)。然后G5在等待。- 最后,
G4釋放寫鎖,G5獲取讀鎖,進(jìn)入臨界區(qū)。
基本用法
RWMutex 中包含了以下的方法:
Lock:獲取寫鎖,如果有其他 goroutine 持有讀鎖或?qū)戞i,那么就會阻塞等待。Unlock:釋放寫鎖。RLock:獲取讀鎖,如果有其他 goroutine 持有寫鎖,那么就會阻塞等待。RUnlock:釋放讀鎖。
其他不常用的方法:
RLocker:返回一個讀鎖,該鎖包含了RLock和RUnlock方法,可以用來獲取讀鎖和釋放讀鎖。TryLock: 嘗試獲取寫鎖,如果獲取成功,返回true,否則返回false。不會阻塞等待。TryRLock: 嘗試獲取讀鎖,如果獲取成功,返回true,否則返回false。不會阻塞等待。
一個簡單的例子
我們可以通過下面的例子來看一下 RWMutex 的基本用法:
package mutex
import (
"sync"
"testing"
)
var config map[string]string
var mu sync.RWMutex
func TestRWMutex(t *testing.T) {
config = make(map[string]string)
// 啟動 10 個 goroutine 來寫
var wg1 sync.WaitGroup
wg1.Add(10)
for i := 0; i < 10; i++ {
go func() {
set("foo", "bar")
wg1.Done()
}()
}
// 啟動 100 個 goroutine 來讀
var wg2 sync.WaitGroup
wg2.Add(100)
for i := 0; i < 100; i++ {
go func() {
get("foo")
wg2.Done()
}()
}
wg1.Wait()
wg2.Wait()
}
// 獲取配置
func get(key string) string {
// 獲取讀鎖,可以多個 goroutine 并發(fā)讀取
mu.RLock()
defer mu.RUnlock()
if v, ok := config[key]; ok {
return v
}
return ""
}
// 設(shè)置配置
func set(key, val string) {
// 獲取寫鎖
mu.Lock()
defer mu.Unlock()
config[key] = val
}上面的例子中,我們啟動了 10 個 goroutine 來寫配置,啟動了 100 個 goroutine 來讀配置。 這跟我們現(xiàn)實開發(fā)中的場景是一樣的,很多時候其實是讀多寫少的。 如果我們在讀的時候也使用互斥鎖,那么就會導(dǎo)致讀的性能非常差,因為讀操作一般都不會有副作用的,但是如果使用互斥鎖,那么就只能一個一個的讀了。
而如果我們使用 RWMutex,那么就可以同時有多個 goroutine 來讀取配置,這樣就可以大大提高讀的性能。 因為我們進(jìn)行讀操作的時候,可以多個 goroutine 并發(fā)讀取,這樣就可以大大提高讀的性能。
RWMutex 使用的注意事項
在《深入理解 go Mutex》中,我們已經(jīng)講過了 Mutex 的使用注意事項, 其實 RWMutex 的使用注意事項也是差不多的:
- 不要忘記釋放鎖,不管是讀鎖還是寫鎖。
Lock之后,沒有釋放鎖之前,不能再次使用Lock。- 在
Unlock之前,必須已經(jīng)調(diào)用了Lock,否則會panic - 在第一次使用
RWMutex之后,不能復(fù)制,因為這樣一來RWMutex的狀態(tài)也會被復(fù)制。這個可以使用go vet來檢查。
源碼剖析
RWMutex 的一些實現(xiàn)原理跟 Mutex 是一樣的,比如阻塞的時候使用信號量等,在 Mutex 那一篇中已經(jīng)有講解了,這里不再贅述。 這里就 RWMutex 的實現(xiàn)原理進(jìn)行一些簡單的剖析。
RWMutex 結(jié)構(gòu)體
RWMutex 的結(jié)構(gòu)體定義如下:
type RWMutex struct {
w Mutex // 互斥鎖,用于保護讀寫鎖的狀態(tài)
writerSem uint32 // writer 信號量
readerSem uint32 // reader 信號量
readerCount atomic.Int32 // 所有 reader 數(shù)量
readerWait atomic.Int32 // writer 等待完成的 reader 數(shù)量
}各字段含義:
w:互斥鎖,用于保護讀寫鎖的狀態(tài)。RWMutex的寫鎖是互斥鎖,所以直接使用Mutex就可以了。writerSem:writer 信號量,用于實現(xiàn)寫鎖的阻塞等待。readerSem:reader 信號量,用于實現(xiàn)讀鎖的阻塞等待。readerCount:所有 reader 數(shù)量(包括已經(jīng)獲取讀鎖的和正在等待獲取讀鎖的 reader)。readerWait:writer 等待完成的 reader 數(shù)量(也就是獲取寫鎖的時刻,已經(jīng)獲取到讀鎖的 reader 數(shù)量)。
因為要區(qū)分讀鎖和寫鎖,所以在 RWMutex 中,我們需要兩個信號量,一個用于實現(xiàn)寫鎖的阻塞等待,一個用于實現(xiàn)讀鎖的阻塞等待。 我們需要特別注意的是 readerCount 和 readerWait 這兩個字段,我們可能會比較好奇,為什么有了 readerCount 這個字段, 還需要 readerWait 這個字段呢?
這是因為,我們在嘗試獲取寫鎖的時候,可能會有多個 reader 正在使用讀鎖,這時候我們需要知道有多少個 reader 正在使用讀鎖, 等待這些 reader 釋放讀鎖之后,就獲取寫鎖了,而 readerWait 這個字段就是用來記錄這個數(shù)量的。 在 Lock 中獲取寫鎖的時候,如果觀測到 readerWait 不為 0 則會阻塞等待,直到 readerWait 為 0 之后才會真正獲取寫鎖,然后才可以進(jìn)行寫操作。
讀鎖源碼剖析
獲取讀鎖的方法如下:
// 獲取讀鎖
func (rw *RWMutex) RLock() {
if rw.readerCount.Add(1) < 0 {
// 有 writer 在使用鎖,阻塞等待 writer 完成
runtime_SemacquireRWMutexR(&rw.readerSem, false, 0)
}
}讀鎖的實現(xiàn)很簡單,先將 readerCount 加 1,如果加 1 之后的值小于 0,說明有 writer 正在使用鎖,那么就需要阻塞等待 writer 完成。
釋放讀鎖的方法如下:
// 釋放讀鎖
func (rw *RWMutex) RUnlock() {
// readerCount 減 1,如果 readerCount 小于 0 說明有 writer 在等待
if r := rw.readerCount.Add(-1); r < 0 {
// 有 writer 在等待,喚醒 writer
rw.rUnlockSlow(r)
}
}
// 喚醒 writer
func (rw *RWMutex) rUnlockSlow(r int32) {
// 未 Lock 就 Unlock,panic
if r+1 == 0 || r+1 == -rwmutexMaxReaders {
fatal("sync: RUnlock of unlocked RWMutex")
}
// readerWait 減 1,返回值是新的 readerWait 值
if rw.readerWait.Add(-1) == 0 {
// 最后一個 reader 喚醒 writer
runtime_Semrelease(&rw.writerSem, false, 1)
}
}讀鎖的實現(xiàn)總結(jié):
- 獲取讀鎖的時候,會將
readerCount加 1 - 如果正在獲取讀鎖的時候,發(fā)現(xiàn)
readerCount小于 0,說明有 writer 正在使用鎖,那么就需要阻塞等待 writer 完成。 - 釋放讀鎖的時候,會將
readerCount減 1 - 如果
readerCount減 1 之后小于 0,說明有 writer 正在等待,那么就需要喚醒 writer。 - 喚醒 writer 的時候,會將
readerWait減 1,如果readerWait減 1 之后為 0,說明 writer 獲取鎖的時候存在的 reader 都已經(jīng)釋放了讀鎖,可以獲取寫鎖了。
·rwmutexMaxReaders算是一個特殊的標(biāo)識,在獲取寫鎖的時候會將readerCount的值減去rwmutexMaxReaders, 所以在其他地方可以根據(jù) readerCount` 是否小于 0 來判斷是否有 writer 正在使用鎖。
寫鎖源碼剖析
獲取寫鎖的方法如下:
// 獲取寫鎖
func (rw *RWMutex) Lock() {
// 首先,解決與其他寫入者的競爭。
rw.w.Lock()
// 向讀者宣布有一個待處理的寫入。
// r 就是當(dāng)前還沒有完成的讀操作,等這部分讀操作完成之后才可以獲取寫鎖。
r := rw.readerCount.Add(-rwmutexMaxReaders) + rwmutexMaxReaders
// 等待活躍的 reader
if r != 0 && rw.readerWait.Add(r) != 0 {
// 阻塞,等待最后一個 reader 喚醒
runtime_SemacquireRWMutex(&rw.writerSem, false, 0)
}
}釋放寫鎖的方法如下:
// 釋放寫鎖
func (rw *RWMutex) Unlock() {
// 向 readers 宣布沒有活動的 writer。
r := rw.readerCount.Add(rwmutexMaxReaders)
if r >= rwmutexMaxReaders { // r >= 0 并且 < rwmutexMaxReaders 才是正常的(r 是持有寫鎖期間嘗試獲取讀鎖的 reader 數(shù)量)
fatal("sync: Unlock of unlocked RWMutex")
}
// 如果有 reader 在等待寫鎖釋放,那么喚醒這些 reader。
for i := 0; i < int(r); i++ {
runtime_Semrelease(&rw.readerSem, false, 0)
}
// 允許其他的 writer 繼續(xù)進(jìn)行。
rw.w.Unlock()
}寫鎖的實現(xiàn)總結(jié):
- 獲取寫鎖的時候,會將
readerCount減去rwmutexMaxReaders,這樣就可以區(qū)分讀鎖和寫鎖了。 - 如果
readerCount減去rwmutexMaxReaders之后不為 0,說明有 reader 正在使用讀鎖,那么就需要阻塞等待這些 reader 釋放讀鎖。 - 釋放寫鎖的時候,會將
readerCount加上rwmutexMaxReaders。 - 如果
readerCount加上rwmutexMaxReaders之后大于 0,說明有 reader 正在等待寫鎖釋放,那么就需要喚醒這些 reader。
TryRLock 和 TryLock
TryRLock 和 TryLock 的實現(xiàn)都很簡單,都是嘗試獲取讀鎖或者寫鎖,如果獲取不到就返回 false,獲取到了就返回 true,這兩個方法不會阻塞等待。
// TryRLock 嘗試鎖定 rw 以進(jìn)行讀取,并報告是否成功。
func (rw *RWMutex) TryRLock() bool {
for {
c := rw.readerCount.Load()
// 有 goroutine 持有寫鎖
if c < 0 {
return false
}
// 嘗試獲取讀鎖
if rw.readerCount.CompareAndSwap(c, c+1) {
return true
}
}
}
// TryLock 嘗試鎖定 rw 以進(jìn)行寫入,并報告是否成功。
func (rw *RWMutex) TryLock() bool {
// 寫鎖被占用
if !rw.w.TryLock() {
return false
}
// 讀鎖被占用
if !rw.readerCount.CompareAndSwap(0, -rwmutexMaxReaders) {
// 釋放寫鎖
rw.w.Unlock()
return false
}
// 成功獲取到鎖
return true
}總結(jié)
RWMutex 使用起來比較簡單,相比 Mutex 而言,它區(qū)分了讀鎖和寫鎖,可以提高并發(fā)性能。最后,總結(jié)一下本文內(nèi)容:
RWMutex 有兩種鎖:讀鎖和寫鎖。
讀鎖可以被多個 goroutine 同時持有,寫鎖只能被一個 goroutine 持有。也就是可以并發(fā)讀,但只能互斥寫。
寫鎖被占用的時候,其他的讀和寫操作都會被阻塞。讀鎖被占用的時候,其他的寫操作會被阻塞,但是讀操作不會被阻塞。除非讀操作發(fā)生在一個新的寫操作之后。
RWMutex 包含以下幾個方法:
Lock:獲取寫鎖,如果有其他的寫鎖或者讀鎖被占用,那么就會阻塞等待。Unlock:釋放寫鎖。RLock:獲取讀鎖,如果寫鎖被占用,那么就會阻塞等待。RUnlock:釋放讀鎖。
也包含了兩個非阻塞的方法:
TryLock:嘗試獲取寫鎖,如果獲取不到就返回false,獲取到了就返回true。TryRLock:嘗試獲取讀鎖,如果獲取不到就返回false,獲取到了就返回true。
RWMutex 使用的注意事項跟 Mutex 差不多:
- 使用之后不能復(fù)制
Unlock之前需要有Lock調(diào)用,否則panic,RUnlock之前需要有RLock調(diào)用,否則panic。- 不要忘記使用
Unlock和RUnlock釋放鎖。
RWMutex 的實現(xiàn):
- 寫鎖還是使用
Mutex來實現(xiàn)。 - 獲取讀鎖和寫鎖的時候,如果獲取不到都會阻塞等待,直到被喚醒。
- 獲取寫鎖的時候,會將
readerCount減去rwmutexMaxReaders,這樣就可以直到有寫鎖被占用。釋放寫鎖的時候,會將readerCount加上rwmutexMaxReaders。 - 獲取寫鎖的時候,如果還有讀操作未完成,那么這一次獲取寫鎖只會等待這部分未完成的讀操作完成。所有后續(xù)的操作只能等待這一次寫鎖釋放。
以上就是一文帶你深入理解Golang中的RWMutex的詳細(xì)內(nèi)容,更多關(guān)于Golang RWMutex的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
一文帶你了解Go語言標(biāo)準(zhǔn)庫math和rand的常用函數(shù)
這篇文章主要為大家詳細(xì)介紹了Go語言標(biāo)準(zhǔn)庫math和rand中的常用函數(shù),文中的示例代碼講解詳細(xì), 對我們學(xué)習(xí)Go語言有一定的幫助,感興趣的小伙伴可以了解一下2022-12-12
Go語言利用time.After實現(xiàn)超時控制的方法詳解
最近在學(xué)習(xí)golang,所以下面這篇文章主要給大家介紹了關(guān)于Go語言利用time.After實現(xiàn)超時控制的相關(guān)資料,文中通過示例介紹的非常詳細(xì),需要的朋友可以參考借鑒,下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-08-08
深入了解Go語言中database/sql是如何設(shè)計的
在?Go?語言中內(nèi)置了?database/sql?包,它只對外暴露了一套統(tǒng)一的編程接口,便可以操作不同數(shù)據(jù)庫,那么database/sql?是如何設(shè)計的呢,下面就來和大家簡單聊聊吧2023-07-07

