Golang并發(fā)之RWMutex的用法詳解
前言
在這篇文章 Go Mutex:保護并發(fā)訪問共享資源的利器 中,主要介紹了 Go
語言中互斥鎖 Mutex
的概念、對應的字段與方法、基本使用和易錯場景,最后基于 Mutex
實現(xiàn)一個簡單的協(xié)程安全的緩存。而本文,我們來看看另一個更高效的 Go
并發(fā)原語,RWMutex
。
準備好了嗎?喝一杯你最喜歡的飲料,隨著文章一起進入 RWMutex
令人興奮的世界!
說明:本文使用的代碼基于的 Go 版本:1.20.1
RWMutex
讀寫互斥鎖是一種同步原語,它允許多個協(xié)程同時訪問共享資源,同時確保一次只有一個協(xié)程可以修改資源。相較于互斥鎖,讀寫互斥鎖在讀操作比寫操作更頻繁的情況下,可以帶來更好的性能表現(xiàn)。
在 Go
語言中,RWMutex
是一種讀寫互斥鎖的實現(xiàn),它提供了一種簡單有效的方式來管理對共享資源的并發(fā)訪問。它提供了兩種類型的鎖:讀鎖 和 寫鎖。
1、讀鎖(RLock()
、TryRLock()
和 RUnlock()
方法)
RWMutex
的讀鎖是一種共享鎖,當一個協(xié)程獲取了讀鎖后,其他協(xié)程也可以同時獲取讀鎖,從而允許并發(fā)的讀操作。
2、寫鎖(Lock()
、TryLock()
和 Unlock()
方法)
RWMutex
的寫鎖是一種獨占鎖,當一個協(xié)程獲取了寫鎖后,其他協(xié)程無法獲取讀鎖或寫鎖,直到該協(xié)程釋放寫鎖。在寫鎖未被釋放之前,任何想要獲取讀鎖或寫鎖的 goroutine
都會被阻塞。
RWMutex 結構體介紹
type RWMutex struct { w Mutex writerSem uint32 // 寫操作等待者 readerSem uint32 // 讀操作等待者 readerCount atomic.Int32 // 持有讀鎖的 goroutine 數(shù)量 readerWait atomic.Int32 // 請求寫鎖時,需要等待完成的讀鎖數(shù)量 }
RWMutex
由以下字段組成:
w
: 為互斥鎖,用于實現(xiàn)寫操作之間的互斥。writerSem
:寫操作的信號量。當有goroutine
請求寫操作時,如果有其他的goroutine
正在執(zhí)行讀操作,則請求寫操作的goroutine
將會被阻塞,直到所有的讀操作完成后,通過writerSem
信號量解除阻塞。readerSem
:讀操作的信號量。當有goroutine
請求讀操作時,如果此時存在寫操作,則請求讀操作的goroutine
將會被阻塞,直到寫操作執(zhí)行完成后,通過readerSem
信號量解除阻塞并繼續(xù)執(zhí)行。readerCount
:讀操作的goroutine
數(shù)量,當readerCount
為正數(shù)時,表示有一個或多個讀操作正在執(zhí)行,如果readerCount
的值為負數(shù),說明有寫操作正在等待。readerWait
:寫操作的goroutine
等待讀操作完成的數(shù)量。當一個寫操作請求執(zhí)行時,如果此時有一個或多個讀操作正在執(zhí)行,則會將讀操作的數(shù)量記錄到readerWait
中,并阻塞寫操作所在的goroutine
。寫操作所在的goroutine
會一直阻塞,直到正在執(zhí)行的所有讀操作完成,此時readerWait
的值將被更新為0
,并且寫操作所在的goroutine
將被喚醒。
RWMutex
常用方法:
Lock()
:獲取寫鎖,擁有寫操作的權限;如果讀操作正在執(zhí)行,此方法將會阻塞,直到所有的讀操作執(zhí)行結束。Unlock()
:釋放寫鎖,并喚醒其他請求讀鎖的goroutine
。TryLock()
:嘗試獲取寫鎖,如果獲取成功,返回true
,否則返回false
,不存在阻塞的情況。RLock()
:獲取讀鎖,讀鎖是共享鎖,可以被多個goroutine
獲取,但是如果有寫操作正在執(zhí)行或等待執(zhí)行時,此方法將會阻塞,直到寫操作執(zhí)行結束。RUnlock()
:釋放讀鎖,如果所有讀操作都結束并且有等待執(zhí)行的寫操作,則會喚醒對應的goroutine
。TryRlock()
:嘗試獲取讀鎖,如果獲取成功,返回true
,否則返回false
,不存在阻塞的情況。
簡單讀寫場景示例
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) } }() } // 寫操作 for { counter.Increment() time.Sleep(time.Second) } }
上述代碼示例中定義了一個 Counter
結構體,包含一個 value
字段和一個 sync.RWMutex
實例 rwMutex
。該結構體還實現(xiàn)了兩個方法:GetValue()
和 Increment()
,分別用于讀取 value
字段的值和對 value
字段的值加一。這兩個方法在訪問 value
字段時,使用了讀寫鎖來保證并發(fā)安全。
在 main()
函數(shù)中,首先創(chuàng)建了一個 Counter
實例 counter
,然后啟動了 10
個協(xié)程,每個協(xié)程會不斷讀取 counter
并打印到控制臺上。同時,main()
函數(shù)也會不斷對 counter
的 value
值加 1
,每次加 1
的操作都會休眠 1
秒鐘。由于使用了讀寫鎖,多個讀操作可以同時進行,而寫操作則會互斥進行,保證了并發(fā)安全。
基于 RWMutex 實現(xiàn)一個簡單的協(xié)程安全的緩存
在 Go Mutex:保護并發(fā)訪問共享資源的利器 文章中,使用了 Mutex
實現(xiàn)了一個簡單的線程安全的緩存,但并不是最優(yōu)的設計,對于緩存場景,讀操作比寫操作更頻繁,因此使用 RWMutex
代替 Mutex
會更好。
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 }
上述代碼實現(xiàn)了一個協(xié)程安全的緩存,通過使用 RWMutex
的讀寫鎖,保證了 Get()
方法可以被多個 goroutine
并發(fā)地執(zhí)行,而且只有在讀操作和寫操作同時存在時才會進行互斥鎖定,有效地提高了并發(fā)性能。
RWMutex 易錯場景
沒有正確的加鎖和解鎖
為了正確使用讀寫鎖,必須正確使用鎖的方法。對于讀操作,必須成對使用 RLock()
和 RUnlock()
方法,否則可能會導致程序 panic
或阻塞。
例如:如果缺少 RLock()
,直接使用 RUnlock()
方法,程序將會 panic
,如果缺少 RUnlock()
方法,將會發(fā)生阻塞的形象。
同樣,對于寫操作,必須成對使用 Lock()
和 Unlock()
方法。
最佳實踐是使用 defer
來釋放鎖:為了保證鎖總是被釋放,即使在運行時錯誤或提前返回的情況下,也可以在獲得鎖后立即使用 defer
關鍵字來調(diào)度相應的解鎖方法。
rwMutex.RLock() defer rwMutex.RUnlock() // 讀操作 rwMutex.Lock() defer rwMutex.Unlock() // 寫操作
重復加鎖
重復加鎖操作被稱為可重入操作。不同于其他一些編程語言的鎖實現(xiàn)(例如 Java
的 ReentrantLock
),Go
的 mutex
并不支持可重入操作。
由于 RWMutex
內(nèi)部是基于 Mutex
實現(xiàn)的寫操作互斥,如果發(fā)生了重復加鎖操作,就會導致死鎖。這個易錯場景在上篇文章中也提到了,還給出了代碼示例,感興趣的小伙伴可以去看看。
讀操作內(nèi)嵌寫操作
當有協(xié)程執(zhí)行讀操作時,請求執(zhí)行寫操作的協(xié)程會被阻塞。如果在讀操作中嵌入寫操作的代碼,寫操作將調(diào)用 Lock()
方法,從而導致讀操作和寫操作之間形成相互依賴關系。在這種情況下,讀操作會等待寫操作完成后才能執(zhí)行 RUnlock()
,而寫操作則會等待讀操作完成后才能被喚醒繼續(xù)執(zhí)行,從而導致死鎖的狀態(tài)。
小結
RWMutex
是 Go
中的一種讀寫鎖實現(xiàn),它通過讀鎖允許多個 goroutine
同時執(zhí)行讀操作,當有寫操作請求時,必須等待所有讀操作執(zhí)行結束后才能執(zhí)行寫操作。
RWMutex
的設計采用了 Write-preferring
方案,即如果有寫操作在等待執(zhí)行,新來的讀操作將會被阻塞,以避免寫操作的饑餓問題。
根據(jù) RWMutex
的特性,它適用于 讀多寫少的高并發(fā)場景,可以實現(xiàn)并發(fā)安全的讀操作,從而減少在鎖競爭中的等待時間。
雖然它能夠給程序帶來了性能的提升,然而,如果使用不當,就可能會導致 panic
或死鎖等問題。因此,在使用 RWMutex
時需要特別小心,并避免錯誤的用法。
以上就是Golang并發(fā)之RWMutex的用法詳解的詳細內(nèi)容,更多關于Golang RWMutex的資料請關注腳本之家其它相關文章!
相關文章
go語言interface接口繼承多態(tài)示例及定義解析
這篇文章主要為大家介紹了go語言interface接口繼承多態(tài)示例及定義解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步早日升職加薪2022-04-04Go語言服務器開發(fā)之客戶端向服務器發(fā)送數(shù)據(jù)并接收返回數(shù)據(jù)的方法
這篇文章主要介紹了Go語言服務器開發(fā)之客戶端向服務器發(fā)送數(shù)據(jù)并接收返回數(shù)據(jù)的方法,實例分析了客戶端的開發(fā)技巧,具有一定參考借鑒價值,需要的朋友可以參考下2015-02-02go 判斷兩個 slice/struct/map 是否相等的實例
這篇文章主要介紹了go 判斷兩個 slice/struct/map 是否相等的實例,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-12-12golang jsoniter extension 處理動態(tài)字段的實現(xiàn)方法
這篇文章主要介紹了golang jsoniter extension 處理動態(tài)字段的實現(xiàn)方法,我們使用實例級別的 extension, 而非全局,可以針對不同業(yè)務邏輯有所區(qū)分,jsoniter 包提供了比較完善的定制能力,通過例子可以感受一下擴展性,需要的朋友可以參考下2023-04-04