[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資料的流程:
- 首先訪問read map,如果read map命中直接返回value
- 檢視amended狀態,如果其為false,說明write map中也沒有這個key,返回空就好了
- 如果amended=true,需要加鎖在訪問一次read map,是一種雙重檢查機制
- 如果read中有了這個key,可能是另一個併發的協程在我們第一次無鎖查詢時已經load了這個key,那麼直接返回value
- 如果read中還是沒有,那麼去讀write,並且把miss+1,然後解鎖並返回結果
- 注意這個miss計數器,當miss計數器的計數長度達到write的大小時,需要將write的kv拷貝給read,然後將write清空
2.3 sync.map Store
Store就是往map中新增新的值或者更新value
- Store會優先訪問read,未命中加鎖訪問write
- Store進行雙重檢查,同樣是因為我們在第一次訪問的同時key已經被放入到了read中
- dirtyLocked在write為nil會從read中拷貝資料,如果read中資料量很大,可能會出現效能抖動
- sync.map不適合頻繁插入新的key-value的場景,因為這種操作會頻繁加鎖訪問
2.4 sync.map Delete
其實可以吧delete視為load的反向操作
- 刪除read中存在的key,可以不用加鎖
- 如果要刪除read中不存在的或者map中不存在的key,都需要加鎖
2.5 sync.map Range
Range可以遍歷map
- Range時,當全部的key都存在於read中是無鎖遍歷的,效率最高
- Range時,如果有部分key存在於write,會加鎖一次性拷貝所有的kv到read中
3. 總結
sync.map更適合多讀的情況,因為多寫場景下會頻繁加鎖而且會發生值拷貝
如果想用多讀的場景,可以考慮開源庫orcaman/concurrent-map,或者如果對效能要求不是很高也可以選擇map+lock的實現方式
參考:
https://cloud.tencent.com/developer/article/1915119