欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Go 并發(fā)讀寫 sync.map 詳細(xì)

 更新時間:2021年10月14日 16:44:39   作者:煎魚  
閱讀本文你將會明確 sync.Map 和原生 map +互斥鎖/讀寫鎖之間的性能情況。標(biāo)準(zhǔn)庫 sync.Map 雖說支持并發(fā)讀寫 map,但更適用于讀多寫少的場景,因為他寫入的性能比較差,使用時要考慮清楚這一點。

map 的兩種目前在業(yè)界使用的最多的并發(fā)支持的模式分別是:

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

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

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

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

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

1、sync.Map 優(yōu)勢

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

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

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

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

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

2、性能測試

聽官方文檔介紹了一堆好處后,他并沒有講到缺點,所說的性能優(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,有興趣的小伙伴可以自己拉下來運(yùn)行試一下。

2.1 壓測結(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

總體的排序(從慢到快)為: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。

2.3 場景分析

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

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

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

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

3、sync.Map 剖析

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

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

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

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
}

  • mu:互斥鎖,用于保護(hù) read dirty。
  • read:只讀數(shù)據(jù),支持并發(fā)讀?。?code>atomic.Value 類型)。如果涉及到更新操作,則只需要加鎖來保證數(shù)據(jù)安全。read 實際存儲的是 readOnly 結(jié)構(gòu)體,內(nèi)部也是一個原生 map,amended 屬性用于標(biāo)記 read dirty 的數(shù)據(jù)是否一致。
  • dirty:讀寫數(shù)據(jù),是一個原生 map,也就是非線程安全。操作 dirty 需要加鎖來保證數(shù)據(jù)安全。
  • misses:統(tǒng)計有多少次讀取 read 沒有命中。每次 read 中讀取失敗后,misses 的計數(shù)值都會加 1。

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

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


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

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

3.2 查找過程

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

sync.Map 的 2 個 map

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

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

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

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

3.3 寫入過程

我們直接關(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ù)安全,也是凸顯性能變差的第一幕。

其分為以下三個處理分支:

  • 若發(fā)現(xiàn) read 中存在該元素,但已經(jīng)被標(biāo)記為已刪除(expunged),則說明 dirty 不等于 nil(dirty 中肯定不存在該元素)。其將會執(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)行處理。

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

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

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

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

3.4 刪除過程

這時候可能有小伙伴在想了。寫入過程,理論上和刪除不會差太遠(yuǎn)。怎么 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ù)進(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,前段時間從字節(jié)大佬分享的案例來看,他們將一個連接作為 key 放了進(jìn)去,于是和這個連接相關(guān)的,例如:buffer 的內(nèi)存就永遠(yuǎn)無法釋放了...

總結(jié):

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

經(jīng)??吹讲l(fā)讀寫 map 導(dǎo)致致命錯誤,實在是令人憂心。大家覺得如果本文不錯,歡迎分享給更多的 Go 愛好者 :)

到此這篇關(guān)于Go 并發(fā)讀寫 sync.map 詳細(xì)的文章就介紹到這了,更多相關(guān)Go 并發(fā)讀寫 sync.map 內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Golang 并發(fā)以及通道的使用方式

    Golang 并發(fā)以及通道的使用方式

    這篇文章主要介紹了Golang 并發(fā)以及通道的使用方式,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2021-03-03
  • windows下安裝make及使用makefile文件

    windows下安裝make及使用makefile文件

    這篇文章主要為大家介紹了windows下安裝make及使用makefile文件方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-01-01
  • golang連接mysql數(shù)據(jù)庫操作使用示例

    golang連接mysql數(shù)據(jù)庫操作使用示例

    這篇文章主要為大家介紹了golang連接mysql數(shù)據(jù)庫操作使用示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步早日升職加薪
    2022-04-04
  • Go語言基礎(chǔ)語法之結(jié)構(gòu)體及方法詳解

    Go語言基礎(chǔ)語法之結(jié)構(gòu)體及方法詳解

    結(jié)構(gòu)體類型可以用來保存不同類型的數(shù)據(jù),也可以通過方法的形式來聲明它的行為。本文將介紹go語言中的結(jié)構(gòu)體和方法,以及“繼承”的實現(xiàn)方法
    2021-09-09
  • 基于Go語言實現(xiàn)冒泡排序算法

    基于Go語言實現(xiàn)冒泡排序算法

    冒泡排序是交換排序中最簡單的一種算法。這篇文章將利用Go語言實現(xiàn)冒泡排序算法,文中的示例代碼講解詳細(xì),對學(xué)習(xí)Go語言有一定的幫助,需要的可以參考一下
    2022-12-12
  • 解析Go語言編程中的struct結(jié)構(gòu)

    解析Go語言編程中的struct結(jié)構(gòu)

    這篇文章主要介紹了Go語言編程中的struct結(jié)構(gòu),是Go語言入門學(xué)習(xí)中的基礎(chǔ)知識,需要的朋友可以參考下
    2015-10-10
  • 定位并修復(fù) Go 中的內(nèi)存泄露問題

    定位并修復(fù) Go 中的內(nèi)存泄露問題

    Go 是一門帶 GC 的語言,這篇文章回顧了我如何發(fā)現(xiàn)內(nèi)存泄漏、如何修復(fù)它,以及我如何修復(fù) Google 示例 Go 代碼中的類似問題,以及我們?nèi)绾胃倪M(jìn)我們的庫以防止將來發(fā)生這種情況,感興趣的朋友一起看看吧
    2021-10-10
  • 一文詳解Golang中的匿名變量

    一文詳解Golang中的匿名變量

    匿名變量是一種特殊類型的變量,可以簡化代碼并提高可讀性,本文將為大家詳細(xì)介紹一下golang中匿名變量的定義、特性和使用方法,需要的可以參考下
    2023-09-09
  • Golang實現(xiàn)超時機(jī)制讀取文件的方法示例

    Golang實現(xiàn)超時機(jī)制讀取文件的方法示例

    讀寫文件是Go程序的基本任務(wù),包括使用程序查看文件內(nèi)容、創(chuàng)建或修改文件,Go提供了os,ioutil,io以及bufio包實現(xiàn)文件操作,本文介紹如果在讀文件過程中增加超時機(jī)制,避免文件太大一直占用資源,需要的朋友可以參考下
    2025-01-01
  • Golang 實現(xiàn) RTP音視頻傳輸示例詳解

    Golang 實現(xiàn) RTP音視頻傳輸示例詳解

    這篇文章主要為大家介紹了Golang實現(xiàn)RTP音視頻傳輸?shù)氖纠斀?,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-07-07

最新評論