偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

一口氣搞懂 Go Sync.Map 所有知識(shí)點(diǎn)

開發(fā) 后端
有了選擇,總是有選擇困難癥的,這兩種到底怎么選,誰的性能更加的好?我有一個(gè)朋友說 標(biāo)準(zhǔn)庫(kù) sync.Map 性能菜的很,不要用。我到底聽誰的...

 [[400078]]

本文轉(zhuǎn)載自微信公眾號(hào)「腦子進(jìn)煎魚了」,作者陳煎魚。轉(zhuǎn)載本文請(qǐng)聯(lián)系腦子進(jìn)煎魚了公眾號(hào)。

大家好,我是煎魚。

在之前的 《為什么 Go map 和 slice 是非線程安全的?》 文章中,我們討論了 Go 語言的 map 和 slice 非線程安全的問題,基于此引申出了 map 的兩種目前在業(yè)界使用的最多的并發(fā)支持的模式。

分別是:

  • 原生 map + 互斥鎖或讀寫鎖 mutex。
  • 標(biāo)準(zhǔn)庫(kù) sync.Map(Go1.9及以后)。

有了選擇,總是有選擇困難癥的,這兩種到底怎么選,誰的性能更加的好?我有一個(gè)朋友說 標(biāo)準(zhǔn)庫(kù) sync.Map 性能菜的很,不要用。我到底聽誰的...

今天煎魚就帶你揭秘 Go sync.map,我們先會(huì)了解清楚什么場(chǎng)景下,Go map 的多種類型怎么用,誰的性能最好!

接著根據(jù)各 map 性能分析的結(jié)果,針對(duì)性的對(duì) sync.map 進(jìn)行源碼解剖,了解 WHY。

一起愉快地開始吸魚之路。

sync.Map 優(yōu)勢(shì)

在 Go 官方文檔中明確指出 Map 類型的一些建議:

  • 多個(gè) goroutine 的并發(fā)使用是安全的,不需要額外的鎖定或協(xié)調(diào)控制。
  • 大多數(shù)代碼應(yīng)該使用原生的 map,而不是單獨(dú)的鎖定或協(xié)調(diào)控制,以獲得更好的類型安全性和維護(hù)性。

同時(shí) Map 類型,還針對(duì)以下場(chǎng)景進(jìn)行了性能優(yōu)化:

  • 當(dāng)一個(gè)給定的鍵的條目只被寫入一次但被多次讀取時(shí)。例如在僅會(huì)增長(zhǎng)的緩存中,就會(huì)有這種業(yè)務(wù)場(chǎng)景。
  • 當(dāng)多個(gè) goroutines 讀取、寫入和覆蓋不相干的鍵集合的條目時(shí)。

這兩種情況與 Go map 搭配單獨(dú)的 Mutex 或 RWMutex 相比較,使用 Map 類型可以大大減少鎖的爭(zhēng)奪。

性能測(cè)試

聽官方文檔介紹了一堆好處后,他并沒有講到缺點(diǎn),所說的性能優(yōu)化后的優(yōu)勢(shì)又是否真實(shí)可信。我們一起來驗(yàn)證一下。

首先我們定義基本的數(shù)據(jù)結(jié)構(gòu):

  1. // 代表互斥鎖 
  2. type FooMap struct { 
  3.  sync.Mutex 
  4.  data map[int]int 
  5.  
  6. // 代表讀寫鎖 
  7. type BarRwMap struct { 
  8.  sync.RWMutex 
  9.  data map[int]int 
  10.  
  11. var fooMap *FooMap 
  12. var barRwMap *BarRwMap 
  13. var syncMap *sync.Map 
  14.  
  15. // 初始化基本數(shù)據(jù)結(jié)構(gòu) 
  16. func init() { 
  17.  fooMap = &FooMap{data: make(map[int]int, 100)} 
  18.  barRwMap = &BarRwMap{data: make(map[int]int, 100)} 
  19.  syncMap = &sync.Map{} 

在配套方法上,常見的增刪改查動(dòng)作我們都編寫了相應(yīng)的方法。用于后續(xù)的壓測(cè)(只展示部分代碼):

  1. func builtinRwMapStore(k, v int) { 
  2.  barRwMap.Lock() 
  3.  defer barRwMap.Unlock() 
  4.  barRwMap.data[k] = v 
  5.  
  6. func builtinRwMapLookup(k intint { 
  7.  barRwMap.RLock() 
  8.  defer barRwMap.RUnlock() 
  9.  if v, ok := barRwMap.data[k]; !ok { 
  10.   return -1 
  11.  } else { 
  12.   return v 
  13.  } 
  14.  
  15. func builtinRwMapDelete(k int) { 
  16.  barRwMap.Lock() 
  17.  defer barRwMap.Unlock() 
  18.  if _, ok := barRwMap.data[k]; !ok { 
  19.   return 
  20.  } else { 
  21.   delete(barRwMap.data, k) 
  22.  } 

其余的類型方法基本類似,考慮重復(fù)篇幅問題因此就不在此展示了。

壓測(cè)方法基本代碼如下:

  1. func BenchmarkBuiltinRwMapDeleteParalell(b *testing.B) { 
  2.  b.RunParallel(func(pb *testing.PB) { 
  3.   r := rand.New(rand.NewSource(time.Now().Unix())) 
  4.   for pb.Next() { 
  5.    k := r.Intn(100000000) 
  6.    builtinRwMapDelete(k) 
  7.   } 
  8.  }) 

這塊主要就是增刪改查的代碼和壓測(cè)方法的準(zhǔn)備,壓測(cè)代碼直接復(fù)用的是大白大佬的 go19-examples/benchmark-for-map 項(xiàng)目。

也可以使用 Go 官方提供的 map_bench_test.go,有興趣的小伙伴可以自己拉下來運(yùn)行試一下。

壓測(cè)結(jié)果

1)寫入:

方法名 含義 壓測(cè)結(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)查找:

方法名 含義 壓測(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+讀寫鎖。最快的是 sync.map 類型。

總體的排序?yàn)椋篗apLookup < 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+讀寫鎖,其次是原生 map+互斥鎖,最快的是 sync.map 類型。

總體的排序?yàn)椋篟wMapDelete < MapDelete < SyncMapDelete。

場(chǎng)景分析

根據(jù)上述的壓測(cè)結(jié)果,我們可以得出 sync.Map 類型:

  • 在讀和刪場(chǎng)景上的性能是最佳的,領(lǐng)先一倍有多。
  • 在寫入場(chǎng)景上的性能非常差,落后原生 map+鎖整整有一倍之多。

因此在實(shí)際的業(yè)務(wù)場(chǎng)景中。假設(shè)是讀多寫少的場(chǎng)景,會(huì)更建議使用 sync.Map 類型。

但若是那種寫多的場(chǎng)景,例如多 goroutine 批量的循環(huán)寫入,那就建議另辟途徑了,性能不忍直視(無性能要求另當(dāng)別論)。

sync.Map 剖析

清楚如何測(cè)試,測(cè)試的結(jié)果后。我們需要進(jìn)一步深挖,知其所以然。

為什么 sync.Map 類型的測(cè)試結(jié)果這么的 “偏科”,為什么讀操作性能這么高,寫操作性能低的可怕,他是怎么設(shè)計(jì)的?

數(shù)據(jù)結(jié)構(gòu)

sync.Map 類型的底層數(shù)據(jù)結(jié)構(gòu)如下:

  1. type Map struct { 
  2.  mu Mutex 
  3.  read atomic.Value // readOnly 
  4.  dirty map[interface{}]*entry 
  5.  misses int 
  6.  
  7. // Map.read 屬性實(shí)際存儲(chǔ)的是 readOnly。 
  8. type readOnly struct { 
  9.  m       map[interface{}]*entry 
  10.  amended bool 
  • mu:互斥鎖,用于保護(hù) read 和 dirty。
  • read:只讀數(shù)據(jù),支持并發(fā)讀取(atomic.Value 類型)。如果涉及到更新操作,則只需要加鎖來保證數(shù)據(jù)安全。
  • read 實(shí)際存儲(chǔ)的是 readOnly 結(jié)構(gòu)體,內(nèi)部也是一個(gè)原生 map,amended 屬性用于標(biāo)記 read 和 dirty 的數(shù)據(jù)是否一致。
  • dirty:讀寫數(shù)據(jù),是一個(gè)原生 map,也就是非線程安全。操作 dirty 需要加鎖來保證數(shù)據(jù)安全。
  • misses:統(tǒng)計(jì)有多少次讀取 read 沒有命中。每次 read 中讀取失敗后,misses 的計(jì)數(shù)值都會(huì)加 1。

在 read 和 dirty 中,都有涉及到的結(jié)構(gòu)體:

  1. type entry struct { 
  2.  p unsafe.Pointer // *interface{} 

其包含一個(gè)指針 p, 用于指向用戶存儲(chǔ)的元素(key)所指向的 value 值。

在此建議你必須搞懂 read、dirty、entry,再往下看,食用效果會(huì)更佳,后續(xù)會(huì)圍繞著這幾個(gè)概念流轉(zhuǎn)。

查找過程

劃重點(diǎn),Map 類型本質(zhì)上是有兩個(gè) “map”。一個(gè)叫 read、一個(gè)叫 dirty,長(zhǎng)的也差不多:

sync.Map 的 2 個(gè) map

當(dāng)我們從 sync.Map 類型中讀取數(shù)據(jù)時(shí),其會(huì)先查看 read 中是否包含所需的元素:

  • 若有,則通過 atomic 原子操作讀取數(shù)據(jù)并返回。
  • 若無,則會(huì)判斷 read.readOnly 中的 amended 屬性,他會(huì)告訴程序 dirty 是否包含 read.readOnly.m 中沒有的數(shù)據(jù);因此若存在,也就是 amended 為 true,將會(huì)進(jìn)一步到 dirty 中查找數(shù)據(jù)。

sync.Map 的讀操作性能如此之高的原因,就在于存在 read 這一巧妙的設(shè)計(jì),其作為一個(gè)緩存層,提供了快路徑(fast path)的查找。

同時(shí)其結(jié)合 amended 屬性,配套解決了每次讀取都涉及鎖的問題,實(shí)現(xiàn)了讀這一個(gè)使用場(chǎng)景的高性能。

寫入過程

我們直接關(guān)注 sync.Map 類型的 Store 方法,該方法的作用是新增或更新一個(gè)元素。

源碼如下:

  1. func (m *Map) Store(key, value interface{}) { 
  2.  read, _ := m.read.Load().(readOnly) 
  3.  if e, ok := read.m[key]; ok && e.tryStore(&value) { 
  4.   return 
  5.  } 
  6.   ... 

調(diào)用 Load 方法檢查 m.read 中是否存在這個(gè)元素。若存在,且沒有被標(biāo)記為刪除狀態(tài),則嘗試存儲(chǔ)。

若該元素不存在或已經(jīng)被標(biāo)記為刪除狀態(tài),則繼續(xù)走到下面流程:

  1. func (m *Map) Store(key, value interface{}) { 
  2.  ... 
  3.  m.mu.Lock() 
  4.  read, _ = m.read.Load().(readOnly) 
  5.  if e, ok := read.m[key]; ok { 
  6.   if e.unexpungeLocked() { 
  7.    m.dirty[key] = e 
  8.   } 
  9.   e.storeLocked(&value) 
  10.  } else if e, ok := m.dirty[key]; ok { 
  11.   e.storeLocked(&value) 
  12.  } else { 
  13.   if !read.amended { 
  14.    m.dirtyLocked() 
  15.    m.read.Store(readOnly{m: read.m, amended: true}) 
  16.   } 
  17.   m.dirty[key] = newEntry(value) 
  18.  } 
  19.  m.mu.Unlock() 

由于已經(jīng)走到了 dirty 的流程,因此開頭就直接調(diào)用了 Lock 方法上互斥鎖,保證數(shù)據(jù)安全,也是凸顯性能變差的第一幕。

其分為以下三個(gè)處理分支:

  • 若發(fā)現(xiàn) read 中存在該元素,但已經(jīng)被標(biāo)記為已刪除(expunged),則說明 dirty 不等于 nil(dirty 中肯定不存在該元素)。其將會(huì)執(zhí)行如下操作。
    • 將元素狀態(tài)從已刪除(expunged)更改為 nil。
    • 將元素插入 dirty 中。
  • 若發(fā)現(xiàn) read 中不存在該元素,但 dirty 中存在該元素,則直接寫入更新 entry 的指向。
  • 若發(fā)現(xiàn) read 和 dirty 都不存在該元素,則從 read 中復(fù)制未被標(biāo)記刪除的數(shù)據(jù),并向 dirty 中插入該元素,賦予元素值 entry 的指向。

我們理一理,寫入過程的整體流程就是:

  • 查 read,read 上沒有,或者已標(biāo)記刪除狀態(tài)。
  • 上互斥鎖(Mutex)。
  • 操作 dirty,根據(jù)各種數(shù)據(jù)情況和狀態(tài)進(jìn)行處理。

回到最初的話題,為什么他寫入性能差那么多。究其原因:

  • 寫入一定要會(huì)經(jīng)過 read,無論如何都比別人多一層,后續(xù)還要查數(shù)據(jù)情況和狀態(tài),性能開銷相較更大。
  • (第三個(gè)處理分支)當(dāng)初始化或者 dirty 被提升后,會(huì)從 read 中復(fù)制全量的數(shù)據(jù),若 read 中數(shù)據(jù)量大,則會(huì)影響性能。

可得知 sync.Map 類型不適合寫多的場(chǎng)景,讀多寫少是比較好的。

若有大數(shù)據(jù)量的場(chǎng)景,則需要考慮 read 復(fù)制數(shù)據(jù)時(shí)的偶然性能抖動(dòng)是否能夠接受。

刪除過程

這時(shí)候可能有小伙伴在想了。寫入過程,理論上和刪除不會(huì)差太遠(yuǎn)。怎么 sync.Map 類型的刪除的性能似乎還行,這里面有什么貓膩?

源碼如下:

  1. func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool) { 
  2.  read, _ := m.read.Load().(readOnly) 
  3.  e, ok := read.m[key
  4.  ... 
  5.   if ok { 
  6.   return e.delete() 
  7.  } 

刪除是標(biāo)準(zhǔn)的開場(chǎng),依然先到 read 檢查該元素是否存在。

若存在,則調(diào)用 delete 標(biāo)記為 expunged(刪除狀態(tài)),非常高效??梢悦鞔_在 read 中的元素,被刪除,性能是非常好的。

若不存在,也就是走到 dirty 流程中:

  1. func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool) { 
  2.  ... 
  3.  if !ok && read.amended { 
  4.   m.mu.Lock() 
  5.   read, _ = m.read.Load().(readOnly) 
  6.   e, ok = read.m[key
  7.   if !ok && read.amended { 
  8.    e, ok = m.dirty[key
  9.    delete(m.dirty, key
  10.    m.missLocked() 
  11.   } 
  12.   m.mu.Unlock() 
  13.  } 
  14.  ... 
  15.  return nil, false 

若 read 中不存在該元素,dirty 不為空,read 與 dirty 不一致(利用 amended 判別),則表明要操作 dirty,上互斥鎖。

再重復(fù)進(jìn)行雙重檢查,若 read 仍然不存在該元素。則調(diào)用 delete 方法從 dirty 中標(biāo)記該元素的刪除。

需要注意,出現(xiàn)頻率較高的 delete 方法:

  1. func (e *entry) delete() (value interface{}, ok bool) { 
  2.  for { 
  3.   p := atomic.LoadPointer(&e.p) 
  4.   if p == nil || p == expunged { 
  5.    return nil, false 
  6.   } 
  7.   if atomic.CompareAndSwapPointer(&e.p, p, nil) { 
  8.    return *(*interface{})(p), true 
  9.   } 
  10.  } 

該方法都是將 entry.p 置為 nil,并且標(biāo)記為 expunged(刪除狀態(tài)),而不是真真正正的刪除。

注:不要誤用 sync.Map,前段時(shí)間從字節(jié)大佬分享的案例來看,他們將一個(gè)連接作為 key 放了進(jìn)去,于是和這個(gè)連接相關(guān)的,例如:buffer 的內(nèi)存就永遠(yuǎn)無法釋放了...

總結(jié)

通過閱讀本文,我們明確了 sync.Map 和原生 map +互斥鎖/讀寫鎖之間的性能情況。

標(biāo)準(zhǔn)庫(kù) sync.Map 雖說支持并發(fā)讀寫 map,但更適用于讀多寫少的場(chǎng)景,因?yàn)樗麑懭氲男阅鼙容^差,使用時(shí)要考慮清楚這一點(diǎn)。

另外我們針對(duì) sync.Map 的性能差異,進(jìn)行了深入的源碼剖析,了解到了其背后快、慢的原因,實(shí)現(xiàn)了知其然知其所以然。

經(jīng)常看到并發(fā)讀寫 map 導(dǎo)致致命錯(cuò)誤,實(shí)在是令人憂心。大家覺得如果本文不錯(cuò),歡迎分享給更多的 Go 愛好者 :)

參考

  • Package sync
  • 踩了 Golang sync.Map 的一個(gè)坑
  • go19-examples/benchmark-for-map
  • 通過實(shí)例深入理解sync.Map的工作原理

 

 

責(zé)任編輯:武曉燕 來源: 腦子進(jìn)煎魚了
相關(guān)推薦

2020-10-22 12:30:33

MySQL

2021-06-08 22:43:07

IPC方式Qt

2020-03-31 08:12:25

Kafka架構(gòu)數(shù)據(jù)庫(kù)

2021-03-29 12:22:25

微信iOS蘋果

2021-12-06 08:30:49

SpringSpring Bean面試題

2025-05-14 01:55:00

FCMCPAI

2024-03-26 09:42:27

分片算法應(yīng)用

2023-12-18 23:09:25

開源優(yōu)化引擎

2020-04-14 13:32:56

@Transacti失效場(chǎng)景

2024-04-26 09:40:10

項(xiàng)目精度丟失javascrip

2020-09-24 09:08:04

分布式系統(tǒng)架構(gòu)

2022-05-24 11:50:46

延時(shí)消息分布式

2020-07-08 07:45:44

OAuth2.0授權(quán)

2024-01-29 00:29:49

通信技術(shù)行業(yè)

2021-01-04 11:23:21

手機(jī)無線電通訊

2021-03-01 18:52:39

工具在線瀏覽器

2020-04-16 12:42:42

附近的人共享單車App

2020-08-12 09:55:07

附近的人數(shù)據(jù)庫(kù)MySQL

2020-10-21 06:39:21

CPU寄存器架構(gòu)

2021-09-30 06:35:23

監(jiān)控性能優(yōu)化
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)