掃二維碼與項目經(jīng)理溝通
我們在微信上24小時期待你的聲音
解答本文疑問/技術(shù)咨詢/運營咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流
本文轉(zhuǎn)載自微信公眾號「腦子進煎魚了」,作者陳煎魚。轉(zhuǎn)載本文請聯(lián)系腦子進煎魚了公眾號。

創(chuàng)新互聯(lián)公司-專業(yè)網(wǎng)站定制、快速模板網(wǎng)站建設(shè)、高性價比匯川網(wǎng)站開發(fā)、企業(yè)建站全套包干低至880元,成熟完善的模板庫,直接使用。一站式匯川網(wǎng)站制作公司更省心,省錢,快速模板網(wǎng)站建設(shè)找我們,業(yè)務(wù)覆蓋匯川地區(qū)。費用合理售后完善,十載實體公司更值得信賴。
大家好,我是煎魚。
在之前的 《為什么 Go map 和 slice 是非線程安全的?》 文章中,我們討論了 Go 語言的 map 和 slice 非線程安全的問題,基于此引申出了 map 的兩種目前在業(yè)界使用的最多的并發(fā)支持的模式。
分別是:
有了選擇,總是有選擇困難癥的,這兩種到底怎么選,誰的性能更加的好?我有一個朋友說 標(biāo)準(zhǔn)庫 sync.Map 性能菜的很,不要用。我到底聽誰的...
今天煎魚就帶你揭秘 Go sync.map,我們先會了解清楚什么場景下,Go map 的多種類型怎么用,誰的性能最好!
接著根據(jù)各 map 性能分析的結(jié)果,針對性的對 sync.map 進行源碼解剖,了解 WHY。
一起愉快地開始吸魚之路。
在 Go 官方文檔中明確指出 Map 類型的一些建議:
同時 Map 類型,還針對以下場景進行了性能優(yōu)化:
這兩種情況與 Go map 搭配單獨的 Mutex 或 RWMutex 相比較,使用 Map 類型可以大大減少鎖的爭奪。
聽官方文檔介紹了一堆好處后,他并沒有講到缺點,所說的性能優(yōu)化后的優(yōu)勢又是否真實可信。我們一起來驗證一下。
首先我們定義基本的數(shù)據(jù)結(jié)構(gòu):
- // 代表互斥鎖
- type FooMap struct {
- sync.Mutex
- data map[int]int
- }
- // 代表讀寫鎖
- type BarRwMap struct {
- sync.RWMutex
- data map[int]int
- }
- var fooMap *FooMap
- var barRwMap *BarRwMap
- var syncMap *sync.Map
- // 初始化基本數(shù)據(jù)結(jié)構(gòu)
- func init() {
- fooMap = &FooMap{data: make(map[int]int, 100)}
- barRwMap = &BarRwMap{data: make(map[int]int, 100)}
- syncMap = &sync.Map{}
- }
在配套方法上,常見的增刪改查動作我們都編寫了相應(yīng)的方法。用于后續(xù)的壓測(只展示部分代碼):
- func builtinRwMapStore(k, v int) {
- barRwMap.Lock()
- defer barRwMap.Unlock()
- barRwMap.data[k] = v
- }
- func builtinRwMapLookup(k int) int {
- barRwMap.RLock()
- defer barRwMap.RUnlock()
- if v, ok := barRwMap.data[k]; !ok {
- return -1
- } else {
- return v
- }
- }
- func builtinRwMapDelete(k int) {
- barRwMap.Lock()
- defer barRwMap.Unlock()
- if _, ok := barRwMap.data[k]; !ok {
- return
- } else {
- delete(barRwMap.data, k)
- }
- }
其余的類型方法基本類似,考慮重復(fù)篇幅問題因此就不在此展示了。
壓測方法基本代碼如下:
- func BenchmarkBuiltinRwMapDeleteParalell(b *testing.B) {
- b.RunParallel(func(pb *testing.PB) {
- r := rand.New(rand.NewSource(time.Now().Unix()))
- for pb.Next() {
- k := r.Intn(100000000)
- builtinRwMapDelete(k)
- }
- })
- }
這塊主要就是增刪改查的代碼和壓測方法的準(zhǔn)備,壓測代碼直接復(fù)用的是大白大佬的 go19-examples/benchmark-for-map 項目。
也可以使用 Go 官方提供的 map_bench_test.go,有興趣的小伙伴可以自己拉下來運行試一下。
壓測結(jié)果
1)寫入:
| 方法名 | 含義 | 壓測結(jié)果 |
|---|---|---|
| BenchmarkBuiltinMapStoreParalell-4 | map+mutex 寫入元素 | 237.1 ns/op |
| BenchmarkSyncMapStoreParalell-4 | sync.map 寫入元素 | 509.3 ns/op |
| BenchmarkBuiltinRwMapStoreParalell-4 | map+rwmutex 寫入元素 | 207.8 ns/op |
在寫入元素上,最慢的是 sync.map 類型,其次是原生 map+互斥鎖(Mutex),最快的是原生 map+讀寫鎖(RwMutex)。
總體的排序(從慢到快)為:SyncMapStore < MapStore < RwMapStore。
2)查找:
| 方法名 | 含義 | 壓測結(jié)果 |
|---|---|---|
| BenchmarkBuiltinMapLookupParalell-4 | map+mutex 查找元素 | 166.7 ns/op |
| BenchmarkBuiltinRwMapLookupParalell-4 | map+rwmutex 查找元素 | 60.49 ns/op |
| BenchmarkSyncMapLookupParalell-4 | sync.map 查找元素 | 53.39 ns/op |
在查找元素上,最慢的是原生 map+互斥鎖,其次是原生 map+讀寫鎖。最快的是 sync.map 類型。
總體的排序為:MapLookup < RwMapLookup < SyncMapLookup。
3)刪除:
| 方法名 | 含義 | 壓測結(jié)果 |
|---|---|---|
| BenchmarkBuiltinMapDeleteParalell-4 | map+mutex 刪除元素 | 168.3 ns/op |
| BenchmarkBuiltinRwMapDeleteParalell-4 | map+rwmutex 刪除元素 | 188.5 ns/op |
| BenchmarkSyncMapDeleteParalell-4 | sync.map 刪除元素 | 41.54 ns/op |
在刪除元素上,最慢的是原生 map+讀寫鎖,其次是原生 map+互斥鎖,最快的是 sync.map 類型。
總體的排序為:RwMapDelete < MapDelete < SyncMapDelete。
根據(jù)上述的壓測結(jié)果,我們可以得出 sync.Map 類型:
因此在實際的業(yè)務(wù)場景中。假設(shè)是讀多寫少的場景,會更建議使用 sync.Map 類型。
但若是那種寫多的場景,例如多 goroutine 批量的循環(huán)寫入,那就建議另辟途徑了,性能不忍直視(無性能要求另當(dāng)別論)。
清楚如何測試,測試的結(jié)果后。我們需要進一步深挖,知其所以然。
為什么 sync.Map 類型的測試結(jié)果這么的 “偏科”,為什么讀操作性能這么高,寫操作性能低的可怕,他是怎么設(shè)計的?
sync.Map 類型的底層數(shù)據(jù)結(jié)構(gòu)如下:
- type Map struct {
- mu Mutex
- read atomic.Value // readOnly
- dirty map[interface{}]*entry
- misses int
- }
- // Map.read 屬性實際存儲的是 readOnly。
- type readOnly struct {
- m map[interface{}]*entry
- amended bool
- }
在 read 和 dirty 中,都有涉及到的結(jié)構(gòu)體:
- type entry struct {
- p unsafe.Pointer // *interface{}
- }
其包含一個指針 p, 用于指向用戶存儲的元素(key)所指向的 value 值。
在此建議你必須搞懂 read、dirty、entry,再往下看,食用效果會更佳,后續(xù)會圍繞著這幾個概念流轉(zhuǎn)。
劃重點,Map 類型本質(zhì)上是有兩個 “map”。一個叫 read、一個叫 dirty,長的也差不多:
sync.Map 的 2 個 map
當(dāng)我們從 sync.Map 類型中讀取數(shù)據(jù)時,其會先查看 read 中是否包含所需的元素:
sync.Map 的讀操作性能如此之高的原因,就在于存在 read 這一巧妙的設(shè)計,其作為一個緩存層,提供了快路徑(fast path)的查找。
同時其結(jié)合 amended 屬性,配套解決了每次讀取都涉及鎖的問題,實現(xiàn)了讀這一個使用場景的高性能。
我們直接關(guān)注 sync.Map 類型的 Store 方法,該方法的作用是新增或更新一個元素。
源碼如下:
- func (m *Map) Store(key, value interface{}) {
- read, _ := m.read.Load().(readOnly)
- if e, ok := read.m[key]; ok && e.tryStore(&value) {
- return
- }
- ...
- }
調(diào)用 Load 方法檢查 m.read 中是否存在這個元素。若存在,且沒有被標(biāo)記為刪除狀態(tài),則嘗試存儲。
若該元素不存在或已經(jīng)被標(biāo)記為刪除狀態(tài),則繼續(xù)走到下面流程:
- func (m *Map) Store(key, value interface{}) {
- ...
- m.mu.Lock()
- read, _ = m.read.Load().(readOnly)
- if e, ok := read.m[key]; ok {
- if e.unexpungeLocked() {
- m.dirty[key] = e
- }
- e.storeLocked(&value)
- } else if e, ok := m.dirty[key]; ok {
- e.storeLocked(&value)
- } else {
- if !read.amended {
- m.dirtyLocked()
- m.read.Store(readOnly{m: read.m, amended: true})
- }
- m.dirty[key] = newEntry(value)
- }
- m.mu.Unlock()
- }
由于已經(jīng)走到了 dirty 的流程,因此開頭就直接調(diào)用了 Lock 方法上互斥鎖,保證數(shù)據(jù)安全,也是凸顯性能變差的第一幕。
其分為以下三個處理分支:
我們理一理,寫入過程的整體流程就是:
回到最初的話題,為什么他寫入性能差那么多。究其原因:
可得知 sync.Map 類型不適合寫多的場景,讀多寫少是比較好的。
若有大數(shù)據(jù)量的場景,則需要考慮 read 復(fù)制數(shù)據(jù)時的偶然性能抖動是否能夠接受。
這時候可能有小伙伴在想了。寫入過程,理論上和刪除不會差太遠。怎么 sync.Map 類型的刪除的性能似乎還行,這里面有什么貓膩?
源碼如下:
- func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool) {
- read, _ := m.read.Load().(readOnly)
- e, ok := read.m[key]
- ...
- if ok {
- return e.delete()
- }
- }
刪除是標(biāo)準(zhǔn)的開場,依然先到 read 檢查該元素是否存在。
若存在,則調(diào)用 delete 標(biāo)記為 expunged(刪除狀態(tài)),非常高效。可以明確在 read 中的元素,被刪除,性能是非常好的。
若不存在,也就是走到 dirty 流程中:
- func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool) {
- ...
- if !ok && read.amended {
- m.mu.Lock()
- read, _ = m.read.Load().(readOnly)
- e, ok = read.m[key]
- if !ok && read.amended {
- e, ok = m.dirty[key]
- delete(m.dirty, key)
- m.missLocked()
- }
- m.mu.Unlock()
- }
- ...
- return nil, false
- }
若 read 中不存在該元素,dirty 不為空,read 與 dirty 不一致(利用 amended 判別),則表明要操作 dirty,上互斥鎖。
再重復(fù)進行雙重檢查,若 read 仍然不存在該元素。則調(diào)用 delete 方法從 dirty 中標(biāo)記該元素的刪除。
需要注意,出現(xiàn)頻率較高的 delete 方法:
- func (e *entry) delete() (value interface{}, ok bool) {
- for {
- p := atomic.LoadPointer(&e.p)
- if p == nil || p == expunged {
- return nil, false
- }
- if atomic.CompareAndSwapPointer(&e.p, p, nil) {
- return *(*interface{})(p), true
- }
- }
- }
該方法都是將 entry.p 置為 nil,并且標(biāo)記為 expunged(刪除狀態(tài)),而不是真真正正的刪除。
注:不要誤用 sync.Map,前段時間從字節(jié)大佬分享的案例來看,他們將一個連接作為 key 放了進去,于是和這個連接相關(guān)的,例如:buffer 的內(nèi)存就永遠無法釋放了...
通過閱讀本文,我們明確了 sync.Map 和原生 map +互斥鎖/讀寫鎖之間的性能情況。
標(biāo)準(zhǔn)庫 sync.Map 雖說支持并發(fā)讀寫 map,但更適用于讀多寫少的場景,因為他寫入的性能比較差,使用時要考慮清楚這一點。
另外我們針對 sync.Map 的性能差異,進行了深入的源碼剖析,了解到了其背后快、慢的原因,實現(xiàn)了知其然知其所以然。
經(jīng)??吹讲l(fā)讀寫 map 導(dǎo)致致命錯誤,實在是令人憂心。大家覺得如果本文不錯,歡迎分享給更多的 Go 愛好者 :)
參考

我們在微信上24小時期待你的聲音
解答本文疑問/技術(shù)咨詢/運營咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流