Golang中,map是引用型別,如切片一樣,通過下面的程式碼宣告後指向的是nil,所以千萬別直接宣告後就使用,新手可能經常會犯如下錯誤:
var m map[string]string
m["result"] = "result"
由於字典是引用型別,所以當我們僅宣告而不初始化一個字典型別的變數的時候,他的值是nil。對值為nil的欄位除新增鍵值對外其他操作都不會引發錯誤。上面的第一行程式碼對其進行寫入操作,就是對空指標的引用,這將會造成一個painc。所以,得記得用 make
函式對其進行分配記憶體和初始化:
m := make(map[string]string)
併發安全也叫執行緒安全,在併發中出現了資料的丟失,稱為併發不安全我們都知道非原子操作的都不是併發安全的,在Golang中map,其讀寫操作並不保證併發安全。如下面的操作
c := make(map[string]string)
wg := sync.WaitGroup{}
for i := 0; i < 10; i++ {
wg.Add(1)
go func(n int) {
k, v := strconv.Itoa(n), strconv.Itoa(n)
c[k] = v
wg.Done()
}(i)
}
wg.Wait()
fmt.Println(c)
執行則會出現下面的錯誤
fatal error: concurrent map writes
你可以通過執行go run --race
來對程式進行競態檢測
那麼如何解決這個問題呢?
通過鎖機制解決併發問題
下面介紹幾種常見的併發安全的操作map的方法
sync.Mutex
c := make(map[string]string)
wg := sync.WaitGroup{}
var lock sync.Mutex
for i := 0; i < 10; i++ {
wg.Add(1)
go func(n int) {
k, v := strconv.Itoa(n), strconv.Itoa(n)
lock.Lock()
c[k] = v
lock.Unlock()
wg.Done()
}(i)
}
wg.Wait()
fmt.Println(c)
當然這裡也可以使用讀寫鎖sync.RWMutex
,這樣併發讀多的場景下效能要好。
sync.Map
sync.Map
是官方出品的併發安全的map,他在內部使用了大量的原子操作來存取鍵和值,並使用了read和dirty二個原生map作為儲存介質,具體實現流程可閱讀相關原始碼。
var m sync.Map
//寫
m.Store("foo", "hello world")
m.Store("slice", []int{1, 2, 3, 4, 5, 6, 7})
m.Store("int", 123)
//讀
m.Load("foo")
m.Load("slice")
m.Load("int")
//刪
m.Delete("int")
第三方map包
第三方包的實現都大同小異,基本上都是使用分離鎖來實現併發安全的,具體分離鎖來實現併發安全的原理可參考下面的延伸閱讀
m := cmap.New()
//寫
m.Set("foo", "hello world")
m.Set("slice", []int{1, 2, 3, 4, 5, 6, 7})
m.Set("int", 1)
//讀
m.Get("foo")
m.Get("slice")
m.Get("int")
m := concurrent.NewConcurrentMap()
m.Put("foo", "hello world")
m.Put("slice", []int{1, 2, 3, 4, 5, 6, 7})
m.Put("int", 1)
//讀
m.Get("foo")
m.Get("slice")
m.Get("int")
探索 ConcurrentHashMap 高併發性的實現機制
本文亦在微信公眾號【小道資訊】釋出,歡迎掃碼關注!