Go 并發(fā)讀寫(xiě) sync.map 詳細(xì)
map 的兩種目前在業(yè)界使用的最多的并發(fā)支持的模式分別是:
- 原生
map +互斥鎖或讀寫(xiě)鎖mutex。 - 標(biāo)準(zhǔn)庫(kù)
sync.Map(Go1.9及以后)。
有了選擇,總是有選擇困難癥的,這兩種到底怎么選,誰(shuí)的性能更加的好?我有一個(gè)朋友說(shuō) 標(biāo)準(zhǔn)庫(kù) sync.Map 性能菜的很,不要用。我到底聽(tīng)誰(shuí)的...
今天煎魚(yú)就帶你揭秘 Go sync.map,我們先會(huì)了解清楚什么場(chǎng)景下,Go map 的多種類(lèi)型怎么用,誰(shuí)的性能最好!
接著根據(jù)各 map 性能分析的結(jié)果,針對(duì)性的對(duì) sync.map 進(jìn)行源碼解剖,了解 WHY。
一起愉快地開(kāi)始吸魚(yú)之路。
1、sync.Map 優(yōu)勢(shì)
在 Go 官方文檔中明確指出 Map 類(lèi)型的一些建議:

- 多個(gè)
goroutine的并發(fā)使用是安全的,不需要額外的鎖定或協(xié)調(diào)控制。 - 大多數(shù)代碼應(yīng)該使用原生的
map,而不是單獨(dú)的鎖定或協(xié)調(diào)控制,以獲得更好的類(lèi)型安全性和維護(hù)性。
同時(shí) Map 類(lèi)型,還針對(duì)以下場(chǎng)景進(jìn)行了性能優(yōu)化:
- 當(dāng)一個(gè)給定的鍵的條目只被寫(xiě)入一次但被多次讀取時(shí)。例如在僅會(huì)增長(zhǎng)的緩存中,就會(huì)有這種業(yè)務(wù)場(chǎng)景。
- 當(dāng)多個(gè)
goroutines讀取、寫(xiě)入和覆蓋不相干的鍵集合的條目時(shí)。
這兩種情況與 Go map 搭配單獨(dú)的 Mutex 或 RWMutex 相比較,使用 Map 類(lèi)型可以大大減少鎖的爭(zhēng)奪。
2、性能測(cè)試
聽(tīng)官方文檔介紹了一堆好處后,他并沒(méi)有講到缺點(diǎn),所說(shuō)的性能優(yōu)化后的優(yōu)勢(shì)又是否真實(shí)可信。我們一起來(lái)驗(yàn)證一下。
首先我們定義基本的數(shù)據(jù)結(jié)構(gòu):
// 代表互斥鎖
type FooMap struct {
sync.Mutex
data map[int]int
}
// 代表讀寫(xiě)鎖
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{}
}
在配套方法上,常見(jiàn)的增刪改查動(dòng)作我們都編寫(xiě)了相應(yīng)的方法。用于后續(xù)的壓測(cè)(只展示部分代碼):
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)
}
}
其余的類(lèi)型方法基本類(lèi)似,考慮重復(fù)篇幅問(wèn)題因此就不在此展示了。
壓測(cè)方法基本代碼如下:
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)
}
})
}
這塊主要就是增刪改查的代碼和壓測(cè)方法的準(zhǔn)備,壓測(cè)代碼直接復(fù)用的是大白大佬的 go19-examples/benchmark-for-map 項(xiàng)目。
也可以使用 Go 官方提供的 map\_bench\_test.go,有興趣的小伙伴可以自己拉下來(lái)運(yùn)行試一下。
2.1 壓測(cè)結(jié)果
1)寫(xiě)入
| 名 | 含義 | 壓測(cè)結(jié)果 |
|---|---|---|
| BenchmarkBuiltinMapStoreParalell-4 | map+mutex 寫(xiě)入元素 | 237.1 ns/op |
| BenchmarkSyncMapStoreParalell-4 | sync.map 寫(xiě)入元素 | 509.3 ns/op |
| BenchmarkBuiltinRwMapStoreParalell-4 | map+rwmutex 寫(xiě)入元素 | 207.8 ns/op |
總體的排序(從慢到快)為:SyncMapStore < MapStore < RwMapStore。
2)查找
| 方法名 | 含義 | 壓測(cè)結(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+讀寫(xiě)鎖。最快的是 sync.map 類(lèi)型。
總體的排序?yàn)椋?code>MapLookup < RwMapLookup < SyncMapLookup。
3)刪除
| 方法名 | 含義 | 壓測(cè)結(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+讀寫(xiě)鎖,其次是原生 map+互斥鎖,最快的是 sync.map 類(lèi)型。
總體的排序?yàn)椋?/strong>RwMapDelete < MapDelete < SyncMapDelete。
2.3 場(chǎng)景分析
根據(jù)上述的壓測(cè)結(jié)果,我們可以得出 sync.Map 類(lèi)型:
- 在讀和刪場(chǎng)景上的性能是最佳的,領(lǐng)先一倍有多。
- 在寫(xiě)入場(chǎng)景上的性能非常差,落后原生 map+鎖整整有一倍之多。
因此在實(shí)際的業(yè)務(wù)場(chǎng)景中。假設(shè)是讀多寫(xiě)少的場(chǎng)景,會(huì)更建議使用 sync.Map 類(lèi)型。
但若是那種寫(xiě)多的場(chǎng)景,例如多 goroutine 批量的循環(huán)寫(xiě)入,那就建議另辟途徑了,性能不忍直視(無(wú)性能要求另當(dāng)別論)。
3、sync.Map 剖析
清楚如何測(cè)試,測(cè)試的結(jié)果后。我們需要進(jìn)一步深挖,知其所以然。
為什么 sync.Map 類(lèi)型的測(cè)試結(jié)果這么的 “偏科”,為什么讀操作性能這么高,寫(xiě)操作性能低的可怕,他是怎么設(shè)計(jì)的?
3.1 數(shù)據(jù)結(jié)構(gòu)
sync.Map 類(lèi)型的底層數(shù)據(jù)結(jié)構(gòu)如下:
type Map struct {
mu Mutex
read atomic.Value // readOnly
dirty map[interface{}]*entry
misses int
}
// Map.read 屬性實(shí)際存儲(chǔ)的是 readOnly。
type readOnly struct {
m map[interface{}]*entry
amended bool
}
mu:互斥鎖,用于保護(hù)read和dirty。read:只讀數(shù)據(jù),支持并發(fā)讀?。?code>atomic.Value 類(lèi)型)。如果涉及到更新操作,則只需要加鎖來(lái)保證數(shù)據(jù)安全。read實(shí)際存儲(chǔ)的是readOnly結(jié)構(gòu)體,內(nèi)部也是一個(gè)原生map,amended屬性用于標(biāo)記read和dirty的數(shù)據(jù)是否一致。dirty:讀寫(xiě)數(shù)據(jù),是一個(gè)原生map,也就是非線程安全。操作dirty需要加鎖來(lái)保證數(shù)據(jù)安全。misses:統(tǒng)計(jì)有多少次讀取read沒(méi)有命中。每次read中讀取失敗后,misses的計(jì)數(shù)值都會(huì)加 1。
在 read 和 dirty 中,都有涉及到的結(jié)構(gòu)體:
type entry struct {
p unsafe.Pointer // *interface{}
}
其包含一個(gè)指針 p, 用于指向用戶(hù)存儲(chǔ)的元素(key)所指向的 value 值。
在此建議你必須搞懂 read、dirty、entry,再往下看,食用效果會(huì)更佳,后續(xù)會(huì)圍繞著這幾個(gè)概念流轉(zhuǎn)。
3.2 查找過(guò)程
劃重點(diǎn),Map 類(lèi)型本質(zhì)上是有兩個(gè) “map”。一個(gè)叫 read、一個(gè)叫 dirty,長(zhǎng)的也差不多:

sync.Map 的 2 個(gè) map
當(dāng)我們從 sync.Map 類(lèi)型中讀取數(shù)據(jù)時(shí),其會(huì)先查看 read 中是否包含所需的元素:
- 若有,則通過(guò)
atomic原子操作讀取數(shù)據(jù)并返回。 - 若無(wú),則會(huì)判斷
read.readOnly中的amended屬性,他會(huì)告訴程序 dirty 是否包含read.readOnly.m中沒(méi)有的數(shù)據(jù);因此若存在,也就是amended為 true,將會(huì)進(jìn)一步到 dirty 中查找數(shù)據(jù)。
sync.Map 的讀操作性能如此之高的原因,就在于存在 read 這一巧妙的設(shè)計(jì),其作為一個(gè)緩存層,提供了快路徑(fast path)的查找。
同時(shí)其結(jié)合 amended 屬性,配套解決了每次讀取都涉及鎖的問(wèn)題,實(shí)現(xiàn)了讀這一個(gè)使用場(chǎng)景的高性能。
3.3 寫(xiě)入過(guò)程
我們直接關(guān)注 sync.Map 類(lèi)型的 Store 方法,該方法的作用是新增或更新一個(gè)元素。
源碼如下:
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 中是否存在這個(gè)元素。若存在,且沒(méi)有被標(biāo)記為刪除狀態(tài),則嘗試存儲(chǔ)。
若該元素不存在或已經(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 的流程,因此開(kāi)頭就直接調(diào)用了 Lock 方法上互斥鎖,保證數(shù)據(jù)安全,也是凸顯性能變差的第一幕。
其分為以下三個(gè)處理分支:
- 若發(fā)現(xiàn) read 中存在該元素,但已經(jīng)被標(biāo)記為已刪除(
expunged),則說(shuō)明dirty不等于nil(dirty 中肯定不存在該元素)。其將會(huì)執(zhí)行如下操作。 - 將元素狀態(tài)從已刪除(
expunged)更改為 nil。 - 將元素插入 dirty 中。
- 若發(fā)現(xiàn) read 中不存在該元素,但 dirty 中存在該元素,則直接寫(xiě)入更新
entry的指向。 - 若發(fā)現(xiàn)
read和dirty都不存在該元素,則從read中復(fù)制未被標(biāo)記刪除的數(shù)據(jù),并向dirty中插入該元素,賦予元素值 entry 的指向。
我們理一理,寫(xiě)入過(guò)程的整體流程就是:
- 查
read,read上沒(méi)有,或者已標(biāo)記刪除狀態(tài)。 - 上互斥鎖(Mutex)。
- 操作 dirty,根據(jù)各種數(shù)據(jù)情況和狀態(tài)進(jìn)行處理。
回到最初的話題,為什么他寫(xiě)入性能差那么多。究其原因:
- 寫(xiě)入一定要會(huì)經(jīng)過(guò)
read,無(wú)論如何都比別人多一層,后續(xù)還要查數(shù)據(jù)情況和狀態(tài),性能開(kāi)銷(xiāo)相較更大。 - (第三個(gè)處理分支)當(dāng)初始化或者
dirty被提升后,會(huì)從read中復(fù)制全量的數(shù)據(jù),若 read 中數(shù)據(jù)量大,則會(huì)影響性能。
可得知 sync.Map 類(lèi)型不適合寫(xiě)多的場(chǎng)景,讀多寫(xiě)少是比較好的。
若有大數(shù)據(jù)量的場(chǎng)景,則需要考慮 read 復(fù)制數(shù)據(jù)時(shí)的偶然性能抖動(dòng)是否能夠接受。
3.4 刪除過(guò)程
這時(shí)候可能有小伙伴在想了。寫(xiě)入過(guò)程,理論上和刪除不會(huì)差太遠(yuǎn)。怎么 sync.Map 類(lèi)型的刪除的性能似乎還行,這里面有什么貓膩?
源碼如下:
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)的開(kāi)場(chǎng),依然先到 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ù)進(jìn)行雙重檢查,若 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,前段時(shí)間從字節(jié)大佬分享的案例來(lái)看,他們將一個(gè)連接作為 key 放了進(jìn)去,于是和這個(gè)連接相關(guān)的,例如:buffer 的內(nèi)存就永遠(yuǎn)無(wú)法釋放了...
總結(jié):
針對(duì) sync.Map 的性能差異,進(jìn)行了深入的源碼剖析,了解到了其背后快、慢的原因,實(shí)現(xiàn)了知其然知其所以然。
經(jīng)??吹讲l(fā)讀寫(xiě) map 導(dǎo)致致命錯(cuò)誤,實(shí)在是令人憂心。大家覺(jué)得如果本文不錯(cuò),歡迎分享給更多的 Go 愛(ài)好者 :)
到此這篇關(guān)于Go 并發(fā)讀寫(xiě) sync.map 詳細(xì)的文章就介紹到這了,更多相關(guān)Go 并發(fā)讀寫(xiě) sync.map 內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
golang連接mysql數(shù)據(jù)庫(kù)操作使用示例
這篇文章主要為大家介紹了golang連接mysql數(shù)據(jù)庫(kù)操作使用示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步早日升職加薪2022-04-04
Go語(yǔ)言基礎(chǔ)語(yǔ)法之結(jié)構(gòu)體及方法詳解
結(jié)構(gòu)體類(lèi)型可以用來(lái)保存不同類(lèi)型的數(shù)據(jù),也可以通過(guò)方法的形式來(lái)聲明它的行為。本文將介紹go語(yǔ)言中的結(jié)構(gòu)體和方法,以及“繼承”的實(shí)現(xiàn)方法2021-09-09
解析Go語(yǔ)言編程中的struct結(jié)構(gòu)
這篇文章主要介紹了Go語(yǔ)言編程中的struct結(jié)構(gòu),是Go語(yǔ)言入門(mén)學(xué)習(xí)中的基礎(chǔ)知識(shí),需要的朋友可以參考下2015-10-10
定位并修復(fù) Go 中的內(nèi)存泄露問(wèn)題
Go 是一門(mén)帶 GC 的語(yǔ)言,這篇文章回顧了我如何發(fā)現(xiàn)內(nèi)存泄漏、如何修復(fù)它,以及我如何修復(fù) Google 示例 Go 代碼中的類(lèi)似問(wèn)題,以及我們?nèi)绾胃倪M(jìn)我們的庫(kù)以防止將來(lái)發(fā)生這種情況,感興趣的朋友一起看看吧2021-10-10
Golang實(shí)現(xiàn)超時(shí)機(jī)制讀取文件的方法示例
讀寫(xiě)文件是Go程序的基本任務(wù),包括使用程序查看文件內(nèi)容、創(chuàng)建或修改文件,Go提供了os,ioutil,io以及bufio包實(shí)現(xiàn)文件操作,本文介紹如果在讀文件過(guò)程中增加超時(shí)機(jī)制,避免文件太大一直占用資源,需要的朋友可以參考下2025-01-01
Golang 實(shí)現(xiàn) RTP音視頻傳輸示例詳解
這篇文章主要為大家介紹了Golang實(shí)現(xiàn)RTP音視頻傳輸?shù)氖纠斀?,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07

