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 同時讀,但是寫的時候,會阻塞其他讀寫操作。新增上 Get
,Set
,Delete
等方法,這個設計是能夠工作的,而且大多數時候能表現不錯。
但是這種設計會有些效能隱患。主要是兩個方面:
- 讀寫鎖的粒度太大了,保護了整個 map 的訪問。寫操作是阻塞的,此時其他任何讀操作都無法進行。
- 如果內部的 map 儲存了很多 key,GC 的時候就需要掃描很久。
「分表」
一種解決思路是“分表”儲存,具體實現就是,基於上面的 syncMap
syncMap
來模擬實現一個 map:
type SyncMap struct {
shardCount uint8
shards []*syncMap
}
上面這種設計用了一個 *syncMap
的 slice 來儲存資料,shardCount
提供了分表量的可定製性。實際上 shards
同樣可以實現為 map[string]*syncMap
。
在這種設計下,資料(key:value)會被分散到不同的 syncMap
,而每個 syncMap
那麼資料如何被分配到指定的分塊呢?一種很通用也很簡單的方法就是 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
.
這種實現足夠簡單,而且也有不錯的效能表現。除了基本的 Get
、Set
、Delete
等基本操作之外,迭代(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是引用型別,同時讀寫共享資源會使得共享資源崩潰