go 併發 map

Gundy發表於2019-07-05

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包

第三方包的實現都大同小異,基本上都是使用分離鎖來實現併發安全的,具體分離鎖來實現併發安全的原理可參考下面的延伸閱讀

concurrent-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")  

go-concurrentMap

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") 

HashMap原理和實現

探索 ConcurrentHashMap 高併發性的實現機制

本文亦在微信公眾號【小道資訊】釋出,歡迎掃碼關注!

相關文章