1. 程式人生 > >go學習(10)併發讀寫訪問map問題

go學習(10)併發讀寫訪問map問題

Golang 裡面 map 不是併發安全的,這一點是眾所周知的,而且官方文件也很早就給瞭解釋:Why are map operations not defined to be atomic?. 也正如這個解釋說的一樣,要實現一個併發安全的 map 其實非常簡單。

併發安全

實際上,大多數情況下,對一個 map 的訪問都是讀操作多於寫操作,而且讀的時候,是可以共享的。所以這種場景下,用一個 sync.RWMutex 保護一下就是很好的選擇:

type syncMap struct {

items map[string]interface{}

sync.RWMutex

}

上面這個結構體定義了一個併發安全的 string map,用一個 map 來儲存資料,一個讀寫鎖來保護安全。這個 map 可以被任意多的 goroutine 同時讀,但是寫的時候,會阻塞其他讀寫操作。新增上 GetSetDelete 等方法,這個設計是能夠工作的,而且大多數時候能表現不錯。

但是這種設計會有些效能隱患。主要是兩個方面:

  1. 讀寫鎖的粒度太大了,保護了整個 map 的訪問。寫操作是阻塞的,此時其他任何讀操作都無法進行。
  2. 如果內部的 map 儲存了很多 key,GC 的時候就需要掃描很久。

 

「分表」

一種解決思路是“分表”儲存,具體實現就是,基於上面的 syncMap

 再包裝一次,用多個 syncMap 來模擬實現一個 map:

type SyncMap struct {

shardCount uint8

shards []*syncMap

}

上面這種設計用了一個 *syncMap 的 slice 來儲存資料,shardCount 提供了分表量的可定製性。實際上 shards 同樣可以實現為 map[string]*syncMap

在這種設計下,資料(key:value)會被分散到不同的 syncMap,而每個 syncMap

 又有自己底層的 map。資料分散了,鎖也分散了,能夠很大程度上提高隨機訪問效能。而且在資料量大、高併發、寫操作頻繁的場景下,這種提升會更加明顯。

那麼資料如何被分配到指定的分塊呢?一種很通用也很簡單的方法就是 hash. 字串的雜湊演算法有很多,byvoid 大神實現和比較了多種字串 hash 函式(各種字串Hash函式比較),得出結論是:“BKDRHash無論是在實際效果還是編碼實現中,效果都是最突出的”。這裡採用了 BKDRHash 來實現:​​​​​​​

const seed uint32 = 131 // 31 131 1313 13131 131313 etc..


func bkdrHash(str string) uint32 {

var h uint32


for _, c := range str {

h = h*seed + uint32(c)

}


return h

}


// Find the specific shard with the given key

func (m *SyncMap) locate(key string) *syncMap {

return m.shards[bkdrHash(key)&uint32((m.shardCount-1))]

}

locate 方法呼叫 bkdrHash 函式計算一個 key 的雜湊值,然後用該值對分表量取模得到在 slice 的 index,之後就能定位到對應的 syncMap.

這種實現足夠簡單,而且也有不錯的效能表現。除了基本的 GetSetDelete 等基本操作之外,迭代(range)功能也非常有用。更多的功能和細節,都可以在原始碼裡找到答案: https://github.com/DeanThompson/syncmap.

還有一點:

如果業務場景能保證我們絕不會同時讀寫一個key的話也不用加鎖,一個小例子試驗下。。

會有衝突的情況:

package main


func main() {

Map := make(map[int]int)


for i := 0; i < 100; i++ {

go writeMap(Map, i, i)

go readMap(Map, i)

}


}


func readMap(Map map[int]int, key int) int {

return Map[key]

}


func writeMap(Map map[int]int, key int, value int) {

Map[key] = value

}

go run -race main.go 結果如下:

那麼稍微改下呢。。

package main


func main() {

Map := make(map[int]int)


for i := 0; i < 2; i++ {

go writeMap(Map, i, i)

go readMap(Map, (i+1) % 2)

}


}


func readMap(Map map[int]int, key int) int {

return Map[key]

}


func writeMap(Map map[int]int, key int, value int) {

Map[key] = value

}

出現的是不衝突,但是沒啥意義,因為幾乎沒有這樣的應用場景吧,在這做這個小測試只是想說明golang 的map會出現讀寫衝突是因為map是引用型別,同時讀寫共享資源會使得共享資源崩潰