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