1. 程式人生 > >golang之map併發訪問

golang之map併發訪問

golang中的map不是併發安全的,併發對map讀寫可能會有問題,如:

// N太小時不會(比如10),因機器而異
// fatal error: concurrent map read and map write
func mapDemo1() {
	m := make(map[string]int)

	go func() {
		for i := 0; i < N; i++ {
			m[strconv.Itoa(i)] = i // write
		}
	}()

	go func() {
		for i := 0; i < N; i++ {
			fmt.Println(i, m[strconv.
Itoa(i)]) // read } }() time.Sleep(time.Second * 5) }

上面的程式碼, 開了兩個goroutine,一個對map寫,一個對map讀。
在N較小時(應該要因機器而異,不是太明確,測試時N=10不會panic),在N較大時(比如1000),就會報錯:

fatal error: concurrent map read and map write

意思就是map併發讀寫是有問題的,會panic掉。

那麼怎麼辦呢?
在go1.9以前,可以加鎖實現:

type Cmap struct {
	m map[string]int
	//lock *sync.Mutex
lock *sync.RWMutex } func (c *Cmap) Get(key string) int { c.lock.RLock() defer c.lock.RUnlock() return c.m[key] } func (c *Cmap) Set(key string, val int) { c.lock.Lock() defer c.lock.Unlock() c.m[key] = val }

這樣的話,Cmap就是自身帶有鎖的map,在讀和寫的位置加鎖就可以:

func mapDemo2() {
	m := make(map[string]int)
	//lock := new(sync.Mutex)
lock := new(sync.RWMutex) cm := Cmap{ m: m, lock: lock, } go func() { for i := 0; i < N; i++ { cm.Set(strconv.Itoa(i), i) } }() go func() { for i := 0; i < N; i++ { fmt.Println(i, cm.Get(strconv.Itoa(i))) } }() time.Sleep(time.Second * 5) }

注意上述的兩句註釋,你可以用sync.Mutex (互斥鎖),或者sync.RWMutex(讀寫鎖, 支援多個讀鎖),對上述程式碼實際執行結果不影響。

不過在go版本1.9開始,有了一個sync.map的東西,對併發做了支援。將上面的例子用sync.map改造如下:

func mapDemo3() {
	var m sync.Map

	go func() {
		for i := 0; i < N; i++ {
			m.Store(strconv.Itoa(i), i) // 寫
		}
	}()

	go func() {
		for i := 0; i < N; i++ {
			v, _ := m.Load(strconv.Itoa(i)) // 讀
			fmt.Println(i, v)
		}
	}()

	time.Sleep(time.Second * 5)
}

可以看到Store就是寫,Load就是讀。
還有幾個方法,說明如下:

  • 刪除
    func (m *Map) Delete(key interface{})

  • 遍歷,類似於js的forEach迴圈
    func (m *Map) Range(f func(key, value interface{}) bool)

  • 存或者取
    func (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool)
    這個方法有點繞,舉例說下:

    var m sync.Map
    
    actual, loaded := m.LoadOrStore("k1", "v1")
    fmt.Println(actual, loaded) // v1 false
    
    actual, loaded = m.LoadOrStore("k1", "v1")
    fmt.Println(actual, loaded) // v1 true
    
    actual, loaded = m.LoadOrStore("k1", "v2")
    fmt.Println(actual, loaded) // // v1 true
    
    actual, loaded = m.LoadOrStore("k2", "v2")
    fmt.Println(actual, loaded) // v2 false
    

    仔細看完上面的例子,就會理解,這個LoadOrStore方法其實是先取,
    如果有key的話就返回,沒有再儲存,其實就是優先取,沒有再儲存
    和快取的做法很類似:先從快取中取,沒有就從資料庫中讀,讀回來存入快取,下次就可以從快取中取到了。

歡迎補充指正!