Go語言之讀寫鎖

karspb發表於2021-09-09

前面的有篇文章在講資源競爭的時候,提到了互斥鎖。互斥鎖的根本就是當一個goroutine訪問的時候,其他goroutine都不能訪問,這樣肯定保證了資源的同步,避免了競爭,不過也降低了效能。


仔細剖析我們的場景,當我們讀取一個資料的時候,如果這個資料永遠不會被修改,那麼其實是不存在資源競爭的問題的。因為資料是不變的,不管怎麼讀取,多少goroutine同時讀取,都是可以的。


所以其實讀取並不是問題,問題主要是修改。修改的資料要同步,這樣其他goroutine才可以感知到。所以真正的互斥應該是讀取和修改、修改和修改之間,讀取和讀取是沒有互斥操作的。


所以這就延伸出來另外一種鎖,叫做讀寫鎖。


讀寫鎖可以讓多個讀操作同時併發,同時讀取,但是對於寫操作是完全互斥的。也就是說,當一個goroutine進行寫操作的時候,其他goroutine既不能進行讀操作,也不能進行寫操作。


var count int
var wg sync.WaitGroup
func main() {    wg.Add(10)    for i:=0;i<5;i++ {            go read(i)    }        for i:=0;i<5;i++ {            go write(i);    }    wg.Wait()}func read(n int) {    fmt.Printf("讀goroutine %d 正在讀取...n",n)    v := count    fmt.Printf("讀goroutine %d 讀取結束,值為:%dn", n,v)    wg.Done()
}
func write(n int) {    fmt.Printf("寫goroutine %d 正在寫入...n",n)    v := rand.Intn(1000)    count = v    fmt.Printf("寫goroutine %d 寫入結束,新值為:%dn", n,v)    wg.Done()
}


以上我們定義了一個共享的資源count,並且宣告瞭兩個函式read和write進行讀寫。在main函式的測試中,我們同時啟動了 5 個讀寫goroutine進行讀寫操作,透過列印的結果來看,寫入操作是處於競爭狀態的,有的寫入操作被覆蓋了。透過go build -race也可以看到更明細的競爭態。


針對這種情況,第一個方案是加互斥鎖,同時只能有一個goroutine可以操作count。但是這種方法效能比較慢,而且我們說的讀操作可以不互斥,所以這種情況比較適合使用讀寫鎖。


var count int
var wg sync.WaitGroup
var rw sync.RWMutex
func main() {    wg.Add(10)    for i:=0;i<5;i++ {            go read(i)    }        for i:=0;i<5;i++ {            go write(i);    }    wg.Wait()}func read(n int) {    rw.RLock()    fmt.Printf("讀goroutine %d 正在讀取...n",n)    v := count    fmt.Printf("讀goroutine %d 讀取結束,值為:%dn", n,v)    wg.Done()    rw.RUnlock()}func write(n int) {    rw.Lock()    fmt.Printf("寫goroutine %d 正在寫入...n",n)    v := rand.Intn(1000)    count = v    fmt.Printf("寫goroutine %d 寫入結束,新值為:%dn", n,v)    wg.Done()    rw.Unlock()
}


我們在read裡使用讀鎖,也就是RLock和RUnlock,寫鎖的方法名和我們平時使用的一樣,是Lock和Unlock。這樣,我們就使用了讀寫鎖,可以併發地讀,但是同時只能有一個寫,並且寫的時候不能進行讀操作。現在我們再執行程式碼,可以從輸出的資料看到,可以讀到新值了。


我們同時也可以使用go build -race檢測,也沒有競爭提示了。


我們在做Java開發的時候,肯定知道SynchronizedMap這個Map,它是一個在多執行緒下安全的Map,我們可以透過Collections.synchronizedMap(Map<K, V>)來獲取一個安全的Map。下面我們看看如何使用讀寫鎖,基於Go語言來實現一個安全的Map。


package common
import (    "sync")
//安全的Map
type SynchronizedMap struct {    rw *sync.RWMutex    data map[interface{}]interface{}
}
//儲存操作
func (sm *SynchronizedMap) Put(k,v interface{}){    sm.rw.Lock()    defer sm.rw.Unlock()    sm.data[k]=v
}
//獲取操作
func (sm *SynchronizedMap) Get(k interface{}) interface{}{    sm.rw.RLock()    defer sm.rw.RUnlock()    return sm.data[k]
}
//刪除操作
func (sm *SynchronizedMap) Delete(k interface{}) {    sm.rw.Lock()    defer sm.rw.Unlock()    delete(sm.data,k)
}
//遍歷Map,並且把遍歷的值給回撥函式,可以讓呼叫者控制做任何事情
func (sm *SynchronizedMap) Each(cb func (interface{},interface{})){    sm.rw.RLock()    defer sm.rw.RUnlock()    for k, v := range sm.data {        cb(k,v)    }
}
//生成初始化一個SynchronizedMap
func NewSynchronizedMap() *SynchronizedMap{    return &SynchronizedMap{        rw:new(sync.RWMutex),        data:make(map[interface{}]interface{}),    }
}


這個安全的Map被我們定義為一個SynchronizedMap的結構體,這個結構體裡有兩個欄位,一個是讀寫鎖rw,一個是儲存資料的data,data是map型別。


然後就是給SynchronizedMap定義一些方法,如果這些方法是增刪改的,就要使用寫鎖;如果是隻讀的,就使用讀鎖。這樣就保證了我們資料data在多個goroutine下的安全性。


有了這個安全的Map我們就可以在多goroutine下增刪改查資料了,都是安全的。


這裡定義了一個Each方法,這個方法很有意思,用過Gradle的都知道,也有類似遍歷Map的方法。這個方法我們可以傳入一個回撥函式作為引數,來對我們遍歷的SynchronizedMap資料進行處理,比如我列印SynchronizedMap中的資料。


sm.Each(func(k interface{}, v interface{}) {    fmt.Println(k," is ",v)
}


sm就是一個SynchronizedMap,非常簡潔吧。


以上就是讀寫鎖使用的一個例子。我們可以把這個map資料當成快取資料,或者當成資料庫,然後使用讀寫鎖進行控制,可以多讀,但是隻能有一個寫。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/2001/viewspace-2817142/,如需轉載,請註明出處,否則將追究法律責任。

相關文章