1. 程式人生 > 其它 >[golang]淺談兩種map

[golang]淺談兩種map

1. golang map

golang原生map在併發場景下,同時讀寫是執行緒不安全的,如論key是否一樣,我們可以編寫一個測試用例來看看同時讀寫不同的key會發生什麼情況:

func testForMap() {
    m := make(map[int]int)
    go func() {
        for {
            m[1] = 1
        }
    }()

    go func() {
        for {
            _ = m[2]
        }

    }()
    select {}
}

func main() {
    testForMap()
}

 

當在終端執行 go run main.go時,會發現系統報錯

fatal error: concurrent map read and map write

 

錯誤很明顯,我們在不同的協程中併發的讀寫了同一個map,雖然是不同的key,還是會發發生併發錯誤,那麼如果想用原生map實現併發操作就必須使用互斥鎖或者讀寫鎖來實現。

我們可以定義一個執行緒安全的map結構體,其中包含了讀寫鎖和一個map:

type SafeMap struct {
    sync.RWMutex
    m map[int]int
}

 

然後就可以併發讀寫這個執行緒安全的map了:

func main() {
    safeMap :
= SafeMap{ m: make(map[int]int), } // 讀資料 safeMap.RLock() data := safeMap.m[1] safeMap.Unlock() fmt.Println(data) // 寫資料 safeMap.Lock() safeMap.m[2] = 1 safeMap.Unlock() }

 

使用讀寫鎖實現的執行緒安全map已經是一種效率較高的map了,我們都知道在併發程式設計中讀寫共享資源加鎖是必須的,即使我們使用了封裝的執行緒安全的資料結構,其底層也是使用了鎖機制,只是在一定程度上對加鎖時機和粒度做了一些優化。

 

2. sync.map

sync.map是用讀寫分離實現的,其思想是空間換時間。和map+lock的實現方式相比,它本身做了一些優化:可以無鎖訪問read map,而且會優先操作read map,如果只操作read map就可以滿足要求,那就不回去操作write map(讀寫加鎖),所以在一些使用場景中它發生鎖競爭的頻率會遠遠小於map+lock的實現方式。

 

2.1 sync.map的定義

type Map struct{
  // 互斥鎖mu,主要是為dirty服務
  mu Mutex
  // read是隻讀資料,可以無鎖訪問
  read atomic.Value
  // 加鎖讀寫,主要處理插入key
  dirty map[interface{}]*entry
  // 統計訪問read未命中然後訪問dirty的次數
  // 用於將dirty提升為read
  misses int
}

 

結構體readOnly,顧名思義這就是一個只讀結構,其實就是上面map定義中的read

type readOnly struct{
  m map[interface{}]*entry
  amended bool
}

其中m就是一個只讀的map,其值entry指標指向真實的資料地址,amended=true表示dirty中有read中不存在的資料

 

2.2 sync.map Load

基本使用我就不放了,就是取值操作,取出key對應的value

我們看一下Load方法的流程圖

 

嘗試簡單分析一下Load資料的流程:

  1. 首先訪問read map,如果read map命中直接返回value
  2. 檢視amended狀態,如果其為false,說明write map中也沒有這個key,返回空就好了
  3. 如果amended=true,需要加鎖在訪問一次read map,是一種雙重檢查機制
  4. 如果read中有了這個key,可能是另一個併發的協程在我們第一次無鎖查詢時已經load了這個key,那麼直接返回value
  5. 如果read中還是沒有,那麼去讀write,並且把miss+1,然後解鎖並返回結果
  6. 注意這個miss計數器,當miss計數器的計數長度達到write的大小時,需要將write的kv拷貝給read,然後將write清空

 

2.3 sync.map Store

Store就是往map中新增新的值或者更新value

 

  1. Store會優先訪問read,未命中加鎖訪問write
  2. Store進行雙重檢查,同樣是因為我們在第一次訪問的同時key已經被放入到了read中
  3. dirtyLocked在write為nil會從read中拷貝資料,如果read中資料量很大,可能會出現效能抖動
  4. sync.map不適合頻繁插入新的key-value的場景,因為這種操作會頻繁加鎖訪問

 

2.4 sync.map Delete

 

其實可以吧delete視為load的反向操作

  1. 刪除read中存在的key,可以不用加鎖
  2. 如果要刪除read中不存在的或者map中不存在的key,都需要加鎖

 

2.5 sync.map Range

Range可以遍歷map

  1. Range時,當全部的key都存在於read中是無鎖遍歷的,效率最高
  2. Range時,如果有部分key存在於write,會加鎖一次性拷貝所有的kv到read中

 

3. 總結

sync.map更適合多讀的情況,因為多寫場景下會頻繁加鎖而且會發生值拷貝

如果想用多讀的場景,可以考慮開源庫orcaman/concurrent-map,或者如果對效能要求不是很高也可以選擇map+lock的實現方式

 

參考:

https://cloud.tencent.com/developer/article/1915119

https://stackoverflow.com/questions/45585589/golang-fatal-error-concurrent-map-read-and-map-write/45585833

https://github.com/golang/go/issues/20680

https://github.com/golang/go/blob/master/src/sync/map.go