Golang并發(fā)之RWMutex的用法詳解
前言
在這篇文章 Go Mutex:保護(hù)并發(fā)訪問(wèn)共享資源的利器 中,主要介紹了 Go 語(yǔ)言中互斥鎖 Mutex 的概念、對(duì)應(yīng)的字段與方法、基本使用和易錯(cuò)場(chǎng)景,最后基于 Mutex 實(shí)現(xiàn)一個(gè)簡(jiǎn)單的協(xié)程安全的緩存。而本文,我們來(lái)看看另一個(gè)更高效的 Go 并發(fā)原語(yǔ),RWMutex。
準(zhǔn)備好了嗎?喝一杯你最喜歡的飲料,隨著文章一起進(jìn)入 RWMutex 令人興奮的世界!
說(shuō)明:本文使用的代碼基于的 Go 版本:1.20.1
RWMutex
讀寫(xiě)互斥鎖是一種同步原語(yǔ),它允許多個(gè)協(xié)程同時(shí)訪問(wèn)共享資源,同時(shí)確保一次只有一個(gè)協(xié)程可以修改資源。相較于互斥鎖,讀寫(xiě)互斥鎖在讀操作比寫(xiě)操作更頻繁的情況下,可以帶來(lái)更好的性能表現(xiàn)。
在 Go 語(yǔ)言中,RWMutex 是一種讀寫(xiě)互斥鎖的實(shí)現(xiàn),它提供了一種簡(jiǎn)單有效的方式來(lái)管理對(duì)共享資源的并發(fā)訪問(wèn)。它提供了兩種類型的鎖:讀鎖 和 寫(xiě)鎖。
1、讀鎖(RLock() 、TryRLock() 和 RUnlock() 方法)
RWMutex 的讀鎖是一種共享鎖,當(dāng)一個(gè)協(xié)程獲取了讀鎖后,其他協(xié)程也可以同時(shí)獲取讀鎖,從而允許并發(fā)的讀操作。
2、寫(xiě)鎖(Lock()、TryLock() 和 Unlock() 方法)
RWMutex 的寫(xiě)鎖是一種獨(dú)占鎖,當(dāng)一個(gè)協(xié)程獲取了寫(xiě)鎖后,其他協(xié)程無(wú)法獲取讀鎖或?qū)戞i,直到該協(xié)程釋放寫(xiě)鎖。在寫(xiě)鎖未被釋放之前,任何想要獲取讀鎖或?qū)戞i的 goroutine 都會(huì)被阻塞。

RWMutex 結(jié)構(gòu)體介紹
type RWMutex struct {
w Mutex
writerSem uint32 // 寫(xiě)操作等待者
readerSem uint32 // 讀操作等待者
readerCount atomic.Int32 // 持有讀鎖的 goroutine 數(shù)量
readerWait atomic.Int32 // 請(qǐng)求寫(xiě)鎖時(shí),需要等待完成的讀鎖數(shù)量
}RWMutex 由以下字段組成:
w: 為互斥鎖,用于實(shí)現(xiàn)寫(xiě)操作之間的互斥。writerSem:寫(xiě)操作的信號(hào)量。當(dāng)有goroutine請(qǐng)求寫(xiě)操作時(shí),如果有其他的goroutine正在執(zhí)行讀操作,則請(qǐng)求寫(xiě)操作的goroutine將會(huì)被阻塞,直到所有的讀操作完成后,通過(guò)writerSem信號(hào)量解除阻塞。readerSem:讀操作的信號(hào)量。當(dāng)有goroutine請(qǐng)求讀操作時(shí),如果此時(shí)存在寫(xiě)操作,則請(qǐng)求讀操作的goroutine將會(huì)被阻塞,直到寫(xiě)操作執(zhí)行完成后,通過(guò)readerSem信號(hào)量解除阻塞并繼續(xù)執(zhí)行。readerCount:讀操作的goroutine數(shù)量,當(dāng)readerCount為正數(shù)時(shí),表示有一個(gè)或多個(gè)讀操作正在執(zhí)行,如果readerCount的值為負(fù)數(shù),說(shuō)明有寫(xiě)操作正在等待。readerWait:寫(xiě)操作的goroutine等待讀操作完成的數(shù)量。當(dāng)一個(gè)寫(xiě)操作請(qǐng)求執(zhí)行時(shí),如果此時(shí)有一個(gè)或多個(gè)讀操作正在執(zhí)行,則會(huì)將讀操作的數(shù)量記錄到readerWait中,并阻塞寫(xiě)操作所在的goroutine。寫(xiě)操作所在的goroutine會(huì)一直阻塞,直到正在執(zhí)行的所有讀操作完成,此時(shí)readerWait的值將被更新為0,并且寫(xiě)操作所在的goroutine將被喚醒。
RWMutex 常用方法:
Lock():獲取寫(xiě)鎖,擁有寫(xiě)操作的權(quán)限;如果讀操作正在執(zhí)行,此方法將會(huì)阻塞,直到所有的讀操作執(zhí)行結(jié)束。Unlock():釋放寫(xiě)鎖,并喚醒其他請(qǐng)求讀鎖的goroutine。TryLock():嘗試獲取寫(xiě)鎖,如果獲取成功,返回true,否則返回false,不存在阻塞的情況。RLock():獲取讀鎖,讀鎖是共享鎖,可以被多個(gè)goroutine獲取,但是如果有寫(xiě)操作正在執(zhí)行或等待執(zhí)行時(shí),此方法將會(huì)阻塞,直到寫(xiě)操作執(zhí)行結(jié)束。RUnlock():釋放讀鎖,如果所有讀操作都結(jié)束并且有等待執(zhí)行的寫(xiě)操作,則會(huì)喚醒對(duì)應(yīng)的goroutine。TryRlock():嘗試獲取讀鎖,如果獲取成功,返回true,否則返回false,不存在阻塞的情況。
簡(jiǎn)單讀寫(xiě)場(chǎng)景示例
package main
import (
"fmt"
"sync"
"time"
)
type Counter struct {
value int
rwMutex sync.RWMutex
}
func (c *Counter) GetValue() int {
c.rwMutex.RLock()
defer c.rwMutex.RUnlock()
return c.value
}
func (c *Counter) Increment() {
c.rwMutex.Lock()
defer c.rwMutex.Unlock()
c.value++
}
func main() {
counter := Counter{value: 0}
// 讀操作
for i := 0; i < 10; i++ {
go func() {
for {
fmt.Println("Value: ", counter.GetValue())
time.Sleep(time.Millisecond)
}
}()
}
// 寫(xiě)操作
for {
counter.Increment()
time.Sleep(time.Second)
}
}上述代碼示例中定義了一個(gè) Counter 結(jié)構(gòu)體,包含一個(gè) value 字段和一個(gè) sync.RWMutex 實(shí)例 rwMutex。該結(jié)構(gòu)體還實(shí)現(xiàn)了兩個(gè)方法:GetValue() 和 Increment(),分別用于讀取 value 字段的值和對(duì) value 字段的值加一。這兩個(gè)方法在訪問(wèn) value 字段時(shí),使用了讀寫(xiě)鎖來(lái)保證并發(fā)安全。
在 main() 函數(shù)中,首先創(chuàng)建了一個(gè) Counter 實(shí)例 counter,然后啟動(dòng)了 10 個(gè)協(xié)程,每個(gè)協(xié)程會(huì)不斷讀取 counter 并打印到控制臺(tái)上。同時(shí),main() 函數(shù)也會(huì)不斷對(duì) counter 的 value 值加 1,每次加 1 的操作都會(huì)休眠 1 秒鐘。由于使用了讀寫(xiě)鎖,多個(gè)讀操作可以同時(shí)進(jìn)行,而寫(xiě)操作則會(huì)互斥進(jìn)行,保證了并發(fā)安全。
基于 RWMutex 實(shí)現(xiàn)一個(gè)簡(jiǎn)單的協(xié)程安全的緩存
在 Go Mutex:保護(hù)并發(fā)訪問(wèn)共享資源的利器 文章中,使用了 Mutex 實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的線程安全的緩存,但并不是最優(yōu)的設(shè)計(jì),對(duì)于緩存場(chǎng)景,讀操作比寫(xiě)操作更頻繁,因此使用 RWMutex 代替 Mutex 會(huì)更好。
import "sync"
type Cache struct {
data map[string]any
rwMutex sync.RWMutex
}
func NewCache() *Cache {
return &Cache{
data: make(map[string]any),
}
}
func (c *Cache) Get(key string) (any, bool) {
c.rwMutex.RLock()
defer c.rwMutex.RUnlock()
value, ok := c.data[key]
return value, ok
}
func (c *Cache) Set(key string, value any) {
c.rwMutex.Lock()
defer c.rwMutex.Unlock()
c.data[key] = value
}上述代碼實(shí)現(xiàn)了一個(gè)協(xié)程安全的緩存,通過(guò)使用 RWMutex 的讀寫(xiě)鎖,保證了 Get() 方法可以被多個(gè) goroutine 并發(fā)地執(zhí)行,而且只有在讀操作和寫(xiě)操作同時(shí)存在時(shí)才會(huì)進(jìn)行互斥鎖定,有效地提高了并發(fā)性能。
RWMutex 易錯(cuò)場(chǎng)景
沒(méi)有正確的加鎖和解鎖
為了正確使用讀寫(xiě)鎖,必須正確使用鎖的方法。對(duì)于讀操作,必須成對(duì)使用 RLock() 和 RUnlock() 方法,否則可能會(huì)導(dǎo)致程序 panic 或阻塞。
例如:如果缺少 RLock(),直接使用 RUnlock()方法,程序?qū)?huì) panic,如果缺少 RUnlock() 方法,將會(huì)發(fā)生阻塞的形象。
同樣,對(duì)于寫(xiě)操作,必須成對(duì)使用 Lock() 和 Unlock() 方法。
最佳實(shí)踐是使用 defer 來(lái)釋放鎖:為了保證鎖總是被釋放,即使在運(yùn)行時(shí)錯(cuò)誤或提前返回的情況下,也可以在獲得鎖后立即使用 defer 關(guān)鍵字來(lái)調(diào)度相應(yīng)的解鎖方法。
rwMutex.RLock() defer rwMutex.RUnlock() // 讀操作 rwMutex.Lock() defer rwMutex.Unlock() // 寫(xiě)操作
重復(fù)加鎖
重復(fù)加鎖操作被稱為可重入操作。不同于其他一些編程語(yǔ)言的鎖實(shí)現(xiàn)(例如 Java 的 ReentrantLock),Go 的 mutex 并不支持可重入操作。
由于 RWMutex 內(nèi)部是基于 Mutex 實(shí)現(xiàn)的寫(xiě)操作互斥,如果發(fā)生了重復(fù)加鎖操作,就會(huì)導(dǎo)致死鎖。這個(gè)易錯(cuò)場(chǎng)景在上篇文章中也提到了,還給出了代碼示例,感興趣的小伙伴可以去看看。
讀操作內(nèi)嵌寫(xiě)操作
當(dāng)有協(xié)程執(zhí)行讀操作時(shí),請(qǐng)求執(zhí)行寫(xiě)操作的協(xié)程會(huì)被阻塞。如果在讀操作中嵌入寫(xiě)操作的代碼,寫(xiě)操作將調(diào)用 Lock() 方法,從而導(dǎo)致讀操作和寫(xiě)操作之間形成相互依賴關(guān)系。在這種情況下,讀操作會(huì)等待寫(xiě)操作完成后才能執(zhí)行 RUnlock(),而寫(xiě)操作則會(huì)等待讀操作完成后才能被喚醒繼續(xù)執(zhí)行,從而導(dǎo)致死鎖的狀態(tài)。
小結(jié)
RWMutex 是 Go 中的一種讀寫(xiě)鎖實(shí)現(xiàn),它通過(guò)讀鎖允許多個(gè) goroutine 同時(shí)執(zhí)行讀操作,當(dāng)有寫(xiě)操作請(qǐng)求時(shí),必須等待所有讀操作執(zhí)行結(jié)束后才能執(zhí)行寫(xiě)操作。
RWMutex 的設(shè)計(jì)采用了 Write-preferring 方案,即如果有寫(xiě)操作在等待執(zhí)行,新來(lái)的讀操作將會(huì)被阻塞,以避免寫(xiě)操作的饑餓問(wèn)題。
根據(jù) RWMutex 的特性,它適用于 讀多寫(xiě)少的高并發(fā)場(chǎng)景,可以實(shí)現(xiàn)并發(fā)安全的讀操作,從而減少在鎖競(jìng)爭(zhēng)中的等待時(shí)間。
雖然它能夠給程序帶來(lái)了性能的提升,然而,如果使用不當(dāng),就可能會(huì)導(dǎo)致 panic 或死鎖等問(wèn)題。因此,在使用 RWMutex 時(shí)需要特別小心,并避免錯(cuò)誤的用法。
以上就是Golang并發(fā)之RWMutex的用法詳解的詳細(xì)內(nèi)容,更多關(guān)于Golang RWMutex的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
細(xì)說(shuō)Go語(yǔ)言中空結(jié)構(gòu)體的奇妙用途
Go語(yǔ)言中,我們可以定義空結(jié)構(gòu)體,即沒(méi)有任何成員變量的結(jié)構(gòu)體,使用關(guān)鍵字?struct{}?來(lái)表示。這種結(jié)構(gòu)體似乎沒(méi)有任何用處,但實(shí)際上它在?Go?語(yǔ)言中的應(yīng)用非常廣泛,本文就來(lái)詳解講講2023-05-05
Golang動(dòng)態(tài)調(diào)用方法小結(jié)
本文主要介紹了Golang動(dòng)態(tài)調(diào)用方法小結(jié),文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-12-12
go語(yǔ)言interface接口繼承多態(tài)示例及定義解析
這篇文章主要為大家介紹了go語(yǔ)言interface接口繼承多態(tài)示例及定義解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步早日升職加薪2022-04-04
golang開(kāi)發(fā)微框架Gin的安裝測(cè)試及簡(jiǎn)介
這篇文章主要為大家介紹了golang微框架Gin的安裝測(cè)試及簡(jiǎn)介,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步早日升職加薪2021-11-11
Go語(yǔ)言服務(wù)器開(kāi)發(fā)之客戶端向服務(wù)器發(fā)送數(shù)據(jù)并接收返回?cái)?shù)據(jù)的方法
這篇文章主要介紹了Go語(yǔ)言服務(wù)器開(kāi)發(fā)之客戶端向服務(wù)器發(fā)送數(shù)據(jù)并接收返回?cái)?shù)據(jù)的方法,實(shí)例分析了客戶端的開(kāi)發(fā)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-02-02
go 判斷兩個(gè) slice/struct/map 是否相等的實(shí)例
這篇文章主要介紹了go 判斷兩個(gè) slice/struct/map 是否相等的實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-12-12
golang jsoniter extension 處理動(dòng)態(tài)字段的實(shí)現(xiàn)方法
這篇文章主要介紹了golang jsoniter extension 處理動(dòng)態(tài)字段的實(shí)現(xiàn)方法,我們使用實(shí)例級(jí)別的 extension, 而非全局,可以針對(duì)不同業(yè)務(wù)邏輯有所區(qū)分,jsoniter 包提供了比較完善的定制能力,通過(guò)例子可以感受一下擴(kuò)展性,需要的朋友可以參考下2023-04-04

